support openID2.0 (#337)
This commit is contained in:
parent
8feb2287cc
commit
ecc736be8b
|
@ -15,6 +15,12 @@ sso:
|
||||||
clientId: ""
|
clientId: ""
|
||||||
clientSecret: ""
|
clientSecret: ""
|
||||||
apiKey: ""
|
apiKey: ""
|
||||||
|
attributes:
|
||||||
|
dispname: "display_name"
|
||||||
|
email: "email"
|
||||||
|
phone: "phone"
|
||||||
|
im: ""
|
||||||
|
coverAttributes: false
|
||||||
|
|
||||||
tokens:
|
tokens:
|
||||||
- rdb-builtin-token
|
- rdb-builtin-token
|
||||||
|
@ -81,4 +87,4 @@ sender:
|
||||||
wechat:
|
wechat:
|
||||||
corp_id: "xxxxxxxxxxxxx"
|
corp_id: "xxxxxxxxxxxxx"
|
||||||
agent_id: 1000000
|
agent_id: 1000000
|
||||||
secret: "xxxxxxxxxxxxxxxxx"
|
secret: "xxxxxxxxxxxxxxxxx"
|
||||||
|
|
|
@ -27,12 +27,19 @@ type wechatSection struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type ssoSection struct {
|
type ssoSection struct {
|
||||||
Enable bool `yaml:"enable"`
|
Enable bool `yaml:"enable"`
|
||||||
RedirectURL string `yaml:"redirectURL"`
|
RedirectURL string `yaml:"redirectURL"`
|
||||||
SsoAddr string `yaml:"ssoAddr"`
|
SsoAddr string `yaml:"ssoAddr"`
|
||||||
ClientId string `yaml:"clientId"`
|
ClientId string `yaml:"clientId"`
|
||||||
ClientSecret string `yaml:"clientSecret"`
|
ClientSecret string `yaml:"clientSecret"`
|
||||||
ApiKey string `yaml:"apiKey"`
|
ApiKey string `yaml:"apiKey"`
|
||||||
|
CoverAttributes bool `yaml:"coverAttributes"`
|
||||||
|
Attributes struct {
|
||||||
|
Dispname string `yaml:"dispname"`
|
||||||
|
Phone string `yaml:"phone"`
|
||||||
|
Email string `yaml:"email"`
|
||||||
|
Im string `yaml:"im"`
|
||||||
|
} `yaml:"attributes"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type httpSection struct {
|
type httpSection struct {
|
||||||
|
|
|
@ -20,6 +20,7 @@ func Config(r *gin.Engine) {
|
||||||
|
|
||||||
notLogin.GET("/auth/authorize", authAuthorize)
|
notLogin.GET("/auth/authorize", authAuthorize)
|
||||||
notLogin.GET("/auth/callback", authCallback)
|
notLogin.GET("/auth/callback", authCallback)
|
||||||
|
notLogin.GET("/auth/settings", authSettings)
|
||||||
}
|
}
|
||||||
|
|
||||||
rootLogin := r.Group("/api/rdb").Use(shouldBeRoot())
|
rootLogin := r.Group("/api/rdb").Use(shouldBeRoot())
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
package http
|
package http
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/url"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/toolkits/pkg/str"
|
"github.com/toolkits/pkg/str"
|
||||||
|
|
||||||
|
@ -107,15 +105,14 @@ func authAuthorize(c *gin.Context) {
|
||||||
if config.Config.SSO.Enable {
|
if config.Config.SSO.Enable {
|
||||||
c.Redirect(302, ssoc.Authorize(redirect))
|
c.Redirect(302, ssoc.Authorize(redirect))
|
||||||
} else {
|
} else {
|
||||||
c.Redirect(302, "/login?redirect="+url.QueryEscape(redirect))
|
c.String(200, "sso does not enable")
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func authCallback(c *gin.Context) {
|
func authCallback(c *gin.Context) {
|
||||||
code := queryStr(c, "code")
|
code := queryStr(c, "code", "")
|
||||||
state := queryStr(c, "state")
|
state := queryStr(c, "state", "")
|
||||||
|
|
||||||
if code == "" {
|
if code == "" {
|
||||||
if redirect := queryStr(c, "redirect"); redirect != "" {
|
if redirect := queryStr(c, "redirect"); redirect != "" {
|
||||||
c.Redirect(302, redirect)
|
c.Redirect(302, redirect)
|
||||||
|
@ -129,3 +126,11 @@ func authCallback(c *gin.Context) {
|
||||||
writeCookieUser(c, user.UUID)
|
writeCookieUser(c, user.UUID)
|
||||||
c.Redirect(302, redirect)
|
c.Redirect(302, redirect)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func authSettings(c *gin.Context) {
|
||||||
|
renderData(c, struct {
|
||||||
|
Sso bool `json:"sso"`
|
||||||
|
}{
|
||||||
|
Sso: config.Config.SSO.Enable,
|
||||||
|
}, nil)
|
||||||
|
}
|
||||||
|
|
|
@ -20,12 +20,20 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type ssoClient struct {
|
type ssoClient struct {
|
||||||
verifier *oidc.IDTokenVerifier
|
verifier *oidc.IDTokenVerifier
|
||||||
config oauth2.Config
|
config oauth2.Config
|
||||||
apiKey string
|
apiKey string
|
||||||
cache *cache.LRUExpireCache
|
cache *cache.LRUExpireCache
|
||||||
ssoAddr string
|
ssoAddr string
|
||||||
callbackAddr string
|
callbackAddr string
|
||||||
|
coverAttributes bool
|
||||||
|
attributes struct {
|
||||||
|
username string
|
||||||
|
dispname string
|
||||||
|
phone string
|
||||||
|
email string
|
||||||
|
im string
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -42,6 +50,12 @@ func InitSSO() {
|
||||||
cli.cache = cache.NewLRUExpireCache(1000)
|
cli.cache = cache.NewLRUExpireCache(1000)
|
||||||
cli.ssoAddr = cf.SsoAddr
|
cli.ssoAddr = cf.SsoAddr
|
||||||
cli.callbackAddr = cf.RedirectURL
|
cli.callbackAddr = cf.RedirectURL
|
||||||
|
cli.coverAttributes = cf.CoverAttributes
|
||||||
|
cli.attributes.username = "sub"
|
||||||
|
cli.attributes.dispname = cf.Attributes.Dispname
|
||||||
|
cli.attributes.phone = cf.Attributes.Phone
|
||||||
|
cli.attributes.email = cf.Attributes.Email
|
||||||
|
cli.attributes.im = cf.Attributes.Im
|
||||||
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)
|
||||||
|
@ -70,18 +84,12 @@ func Authorize(redirect string) string {
|
||||||
|
|
||||||
// LogoutLocation return logout location
|
// LogoutLocation return logout location
|
||||||
func LogoutLocation(redirect string) string {
|
func LogoutLocation(redirect string) string {
|
||||||
redirect = fmt.Sprintf("%s?redriect=%s", cli.callbackAddr,
|
redirect = fmt.Sprintf("%s?redirect=%s", cli.callbackAddr,
|
||||||
url.QueryEscape(redirect))
|
url.QueryEscape(redirect))
|
||||||
return fmt.Sprintf("%s/account/logout?redirect=%s", cli.ssoAddr,
|
return fmt.Sprintf("%s/api/v1/account/logout?redirect=%s", cli.ssoAddr,
|
||||||
url.QueryEscape(redirect))
|
url.QueryEscape(redirect))
|
||||||
}
|
}
|
||||||
|
|
||||||
type tokenClaims struct {
|
|
||||||
Username string `json:"sub"`
|
|
||||||
Email string `json:"email"`
|
|
||||||
DisplayName string `json:"display_name"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Callback 用 code 兑换 accessToken 以及 用户信息,
|
// Callback 用 code 兑换 accessToken 以及 用户信息,
|
||||||
func Callback(code, state string) (string, *models.User, error) {
|
func Callback(code, state string) (string, *models.User, error) {
|
||||||
s, ok := cli.cache.Get(state)
|
s, ok := cli.cache.Get(state)
|
||||||
|
@ -93,35 +101,67 @@ func Callback(code, state string) (string, *models.User, error) {
|
||||||
redirect := s.(string)
|
redirect := s.(string)
|
||||||
log.Printf("callback, get state %s redirect %s", state, redirect)
|
log.Printf("callback, get state %s redirect %s", state, redirect)
|
||||||
|
|
||||||
|
u, err := exchangeUser(code)
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
log.Printf("exchange user %v", u)
|
||||||
|
|
||||||
|
user, err := models.UserGet("username=?", u.Username)
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if user == nil {
|
||||||
|
user = u
|
||||||
|
err = user.Save()
|
||||||
|
} else if cli.coverAttributes {
|
||||||
|
user.Email = u.Email
|
||||||
|
user.Dispname = u.Dispname
|
||||||
|
user.Phone = u.Phone
|
||||||
|
user.Im = u.Im
|
||||||
|
err = user.Update("email", "dispname", "phone", "im")
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect, user, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func exchangeUser(code string) (*models.User, error) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
oauth2Token, err := cli.config.Exchange(ctx, code)
|
oauth2Token, err := cli.config.Exchange(ctx, code)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", nil, fmt.Errorf("Failed to exchange token: %s", err)
|
return nil, fmt.Errorf("Failed to exchange token: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
rawIDToken, ok := oauth2Token.Extra("id_token").(string)
|
rawIDToken, ok := oauth2Token.Extra("id_token").(string)
|
||||||
if !ok {
|
if !ok {
|
||||||
return "", nil, fmt.Errorf("No id_token field in oauth2 token.")
|
return nil, fmt.Errorf("No id_token field in oauth2 token.")
|
||||||
}
|
}
|
||||||
idToken, err := cli.verifier.Verify(ctx, rawIDToken)
|
idToken, err := cli.verifier.Verify(ctx, rawIDToken)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", nil, fmt.Errorf("Failed to verify ID Token: %s", err)
|
return nil, fmt.Errorf("Failed to verify ID Token: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
data := &tokenClaims{}
|
data := map[string]interface{}{}
|
||||||
if err := idToken.Claims(data); err != nil {
|
if err := idToken.Claims(&data); err != nil {
|
||||||
return "", nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := models.UserGet("username=?", data.Username)
|
v := func(k string) string {
|
||||||
if err != nil {
|
if in := data[k]; in == nil {
|
||||||
return "", nil, err
|
return ""
|
||||||
}
|
} else {
|
||||||
if user == nil {
|
return in.(string)
|
||||||
return "", nil, fmt.Errorf("user %s is not found", data.Username)
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return redirect, user, nil
|
return &models.User{
|
||||||
|
Username: v(cli.attributes.username),
|
||||||
|
Dispname: v(cli.attributes.dispname),
|
||||||
|
Phone: v(cli.attributes.phone),
|
||||||
|
Email: v(cli.attributes.email),
|
||||||
|
Im: v(cli.attributes.im),
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func CreateClient(w http.ResponseWriter, body io.ReadCloser) error {
|
func CreateClient(w http.ResponseWriter, body io.ReadCloser) error {
|
||||||
|
@ -141,12 +181,12 @@ func GetClient(w http.ResponseWriter, clientId string) error {
|
||||||
|
|
||||||
func UpdateClient(w http.ResponseWriter, clientId string, body io.ReadCloser) error {
|
func UpdateClient(w http.ResponseWriter, clientId string, body io.ReadCloser) error {
|
||||||
u := mkUrl("/api/v1/clients/"+clientId, nil)
|
u := mkUrl("/api/v1/clients/"+clientId, nil)
|
||||||
return req("GET", u, body, w)
|
return req("PUT", u, body, w)
|
||||||
}
|
}
|
||||||
|
|
||||||
func DeleteClient(w http.ResponseWriter, clientId string) error {
|
func DeleteClient(w http.ResponseWriter, clientId string) error {
|
||||||
u := mkUrl("/api/v1/clients/"+clientId, nil)
|
u := mkUrl("/api/v1/clients/"+clientId, nil)
|
||||||
return req("GET", u, nil, w)
|
return req("DELETE", u, nil, w)
|
||||||
}
|
}
|
||||||
|
|
||||||
func mkUrl(api string, query url.Values) string {
|
func mkUrl(api string, query url.Values) string {
|
||||||
|
|
Loading…
Reference in New Issue