support openID2.0 (#337)

This commit is contained in:
yubo 2020-10-14 13:30:53 +08:00 committed by GitHub
parent 8feb2287cc
commit ecc736be8b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 101 additions and 42 deletions

View File

@ -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"

View File

@ -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 {

View File

@ -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())

View File

@ -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)
}

View File

@ -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 {