From 051752340d21470db4020b9cf1b46fa50ae908a9 Mon Sep 17 00:00:00 2001 From: Steve0x2a Date: Wed, 26 Jan 2022 11:56:01 +0800 Subject: [PATCH] feat: add userinfo endpoint (#447) * feat: add userinfo endpoint Signed-off-by: 0x2a * feat: add scope support Signed-off-by: 0x2a * fix: modify the endpoint of discovery Signed-off-by: 0x2a --- authz/authz.go | 1 + controllers/account.go | 55 +++++++++++++++++++++++++++++++++++ controllers/base.go | 22 ++++++++++++++ object/oidc_discovery.go | 2 +- object/token.go | 6 ++-- object/token_jwt.go | 8 +++-- routers/auto_signin_filter.go | 2 ++ routers/base.go | 12 ++++++++ routers/router.go | 1 + 9 files changed, 103 insertions(+), 6 deletions(-) diff --git a/authz/authz.go b/authz/authz.go index 503298ef50b6..def22efaa86a 100644 --- a/authz/authz.go +++ b/authz/authz.go @@ -80,6 +80,7 @@ p, *, *, POST, /api/login, *, * p, *, *, GET, /api/get-app-login, *, * p, *, *, POST, /api/logout, *, * p, *, *, GET, /api/get-account, *, * +p, *, *, GET, /api/userinfo, *, * p, *, *, POST, /api/login/oauth/access_token, *, * p, *, *, POST, /api/login/oauth/refresh_token, *, * p, *, *, GET, /api/get-application, *, * diff --git a/controllers/account.go b/controllers/account.go index e920daa27ec1..fed9a39cc178 100644 --- a/controllers/account.go +++ b/controllers/account.go @@ -18,7 +18,9 @@ import ( "encoding/json" "fmt" "strconv" + "strings" + "github.com/astaxie/beego" "github.com/casdoor/casdoor/object" "github.com/casdoor/casdoor/util" ) @@ -67,6 +69,18 @@ type Response struct { Data2 interface{} `json:"data2"` } +type Userinfo struct { + Sub string `json:"sub"` + Iss string `json:"iss"` + Aud string `json:"aud"` + Name string `json:"name,omitempty"` + DisplayName string `json:"preferred_username,omitempty"` + Email string `json:"email,omitempty"` + Avatar string `json:"picture,omitempty"` + Address string `json:"address,omitempty"` + Phone string `json:"phone,omitempty"` +} + type HumanCheck struct { Type string `json:"type"` AppKey string `json:"appKey"` @@ -231,6 +245,47 @@ func (c *ApiController) GetAccount() { c.ServeJSON() } +// UserInfo +// @Title UserInfo +// @Tag Account API +// @Description return user information according to OIDC standards +// @Success 200 {object} controllers.Userinfo The Response object +// @router /userinfo [get] +func (c *ApiController) GetUserinfo() { + userId, ok := c.RequireSignedIn() + if !ok { + return + } + user := object.GetUser(userId) + if user == nil { + c.ResponseError(fmt.Sprintf("The user: %s doesn't exist", userId)) + return + } + scope, aud := c.GetSessionOidc() + iss := beego.AppConfig.String("origin") + resp := Userinfo{ + Sub: user.Id, + Iss: iss, + Aud: aud, + } + if strings.Contains(scope, "profile") { + resp.Name = user.Name + resp.DisplayName = user.DisplayName + resp.Avatar = user.Avatar + } + if strings.Contains(scope, "email") { + resp.Email = user.Email + } + if strings.Contains(scope, "address") { + resp.Address = user.Location + } + if strings.Contains(scope, "phone") { + resp.Phone = user.Phone + } + c.Data["json"] = resp + c.ServeJSON() +} + // GetHumanCheck ... // @Tag Login API // @Title GetHumancheck diff --git a/controllers/base.go b/controllers/base.go index 804975c9d89d..6a9eb9d3458d 100644 --- a/controllers/base.go +++ b/controllers/base.go @@ -72,6 +72,28 @@ func (c *ApiController) GetSessionUsername() string { return user.(string) } +func (c *ApiController) GetSessionOidc() (string, string) { + sessionData := c.GetSessionData() + if sessionData != nil && + sessionData.ExpireTime != 0 && + sessionData.ExpireTime < time.Now().Unix() { + c.SetSessionUsername("") + c.SetSessionData(nil) + return "", "" + } + scopeValue := c.GetSession("scope") + audValue := c.GetSession("aud") + var scope, aud string + var ok bool + if scope, ok = scopeValue.(string); !ok { + scope = "" + } + if aud, ok = audValue.(string); !ok { + aud = "" + } + return scope, aud +} + // SetSessionUsername ... func (c *ApiController) SetSessionUsername(user string) { c.SetSession("username", user) diff --git a/object/oidc_discovery.go b/object/oidc_discovery.go index 2cc9f17292cb..cb0b76a5c87c 100644 --- a/object/oidc_discovery.go +++ b/object/oidc_discovery.go @@ -54,7 +54,7 @@ func init() { Issuer: origin, AuthorizationEndpoint: fmt.Sprintf("%s/login/oauth/authorize", origin), TokenEndpoint: fmt.Sprintf("%s/api/login/oauth/access_token", origin), - UserinfoEndpoint: fmt.Sprintf("%s/api/get-account", origin), + UserinfoEndpoint: fmt.Sprintf("%s/api/userinfo", origin), JwksUri: fmt.Sprintf("%s/api/certs", origin), ResponseTypesSupported: []string{"id_token"}, ResponseModesSupported: []string{"login", "code", "link"}, diff --git a/object/token.go b/object/token.go index 7770f80ca55d..6d87b29bf445 100644 --- a/object/token.go +++ b/object/token.go @@ -208,12 +208,12 @@ func GetOAuthCode(userId string, clientId string, responseType string, redirectU } } - accessToken, refreshToken, err := generateJwtToken(application, user, nonce) + accessToken, refreshToken, err := generateJwtToken(application, user, nonce, scope) if err != nil { panic(err) } - if challenge == "null"{ + if challenge == "null" { challenge = "" } @@ -376,7 +376,7 @@ func RefreshToken(grantType string, refreshToken string, scope string, clientId Scope: "", } } - newAccessToken, newRefreshToken, err := generateJwtToken(application, user, "") + newAccessToken, newRefreshToken, err := generateJwtToken(application, user, "", scope) if err != nil { panic(err) } diff --git a/object/token_jwt.go b/object/token_jwt.go index 748e11cc2f51..21deb8b8768e 100644 --- a/object/token_jwt.go +++ b/object/token_jwt.go @@ -27,6 +27,7 @@ type Claims struct { *User Nonce string `json:"nonce,omitempty"` Tag string `json:"tag,omitempty"` + Scope string `json:"scope,omitempty"` jwt.RegisteredClaims } @@ -38,6 +39,7 @@ type UserShort struct { type ClaimsShort struct { *UserShort Nonce string `json:"nonce,omitempty"` + Scope string `json:"scope,omitempty"` jwt.RegisteredClaims } @@ -53,12 +55,13 @@ func getShortClaims(claims Claims) ClaimsShort { res := ClaimsShort{ UserShort: getShortUser(claims.User), Nonce: claims.Nonce, + Scope: claims.Scope, RegisteredClaims: claims.RegisteredClaims, } return res } -func generateJwtToken(application *Application, user *User, nonce string) (string, string, error) { +func generateJwtToken(application *Application, user *User, nonce string, scope string) (string, string, error) { nowTime := time.Now() expireTime := nowTime.Add(time.Duration(application.ExpireInHours) * time.Hour) refreshExpireTime := nowTime.Add(time.Duration(application.RefreshExpireInHours) * time.Hour) @@ -69,7 +72,8 @@ func generateJwtToken(application *Application, user *User, nonce string) (strin User: user, Nonce: nonce, // FIXME: A workaround for custom claim by reusing `tag` in user info - Tag: user.Tag, + Tag: user.Tag, + Scope: scope, RegisteredClaims: jwt.RegisteredClaims{ Issuer: beego.AppConfig.String("origin"), Subject: user.Id, diff --git a/routers/auto_signin_filter.go b/routers/auto_signin_filter.go index 1cbb2ba2913d..de33774b24a2 100644 --- a/routers/auto_signin_filter.go +++ b/routers/auto_signin_filter.go @@ -43,6 +43,7 @@ func AutoSigninFilter(ctx *context.Context) { userId := fmt.Sprintf("%s/%s", claims.User.Owner, claims.User.Name) setSessionUser(ctx, userId) + setSessionOidc(ctx, claims.Scope, claims.Audience[0]) return } @@ -81,5 +82,6 @@ func AutoSigninFilter(ctx *context.Context) { setSessionUser(ctx, fmt.Sprintf("%s/%s", claims.Owner, claims.Name)) setSessionExpire(ctx, claims.ExpiresAt.Unix()) + setSessionOidc(ctx, claims.Scope, claims.Audience[0]) } } diff --git a/routers/base.go b/routers/base.go index f478bcf42d7f..df4ab19609c3 100644 --- a/routers/base.go +++ b/routers/base.go @@ -97,6 +97,18 @@ func setSessionExpire(ctx *context.Context, ExpireTime int64) { ctx.Input.CruSession.SessionRelease(ctx.ResponseWriter) } +func setSessionOidc(ctx *context.Context, scope string, aud string) { + err := ctx.Input.CruSession.Set("scope", scope) + if err != nil { + panic(err) + } + err = ctx.Input.CruSession.Set("aud", aud) + if err != nil { + panic(err) + } + ctx.Input.CruSession.SessionRelease(ctx.ResponseWriter) +} + func parseBearerToken(ctx *context.Context) string { header := ctx.Request.Header.Get("Authorization") tokens := strings.Split(header, " ") diff --git a/routers/router.go b/routers/router.go index 0b14c408295d..348818bccd06 100644 --- a/routers/router.go +++ b/routers/router.go @@ -50,6 +50,7 @@ func initAPI() { beego.Router("/api/get-app-login", &controllers.ApiController{}, "GET:GetApplicationLogin") beego.Router("/api/logout", &controllers.ApiController{}, "POST:Logout") beego.Router("/api/get-account", &controllers.ApiController{}, "GET:GetAccount") + beego.Router("/api/userinfo", &controllers.ApiController{}, "GET:GetUserinfo") beego.Router("/api/unlink", &controllers.ApiController{}, "POST:Unlink") beego.Router("/api/get-saml-login", &controllers.ApiController{}, "GET:GetSamlLogin") beego.Router("/api/acs", &controllers.ApiController{}, "POST:HandleSamlLogin")