Skip to content

Commit

Permalink
feat: add userinfo endpoint (casdoor#447)
Browse files Browse the repository at this point in the history
* feat: add userinfo endpoint

Signed-off-by: 0x2a <[email protected]>

* feat: add scope support

Signed-off-by: 0x2a <[email protected]>

* fix: modify the endpoint of discovery

Signed-off-by: 0x2a <[email protected]>
  • Loading branch information
Steve0x2a authored Jan 26, 2022
1 parent c87c001 commit 0517523
Show file tree
Hide file tree
Showing 9 changed files with 103 additions and 6 deletions.
1 change: 1 addition & 0 deletions authz/authz.go
Original file line number Diff line number Diff line change
Expand Up @@ -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, *, *
Expand Down
55 changes: 55 additions & 0 deletions controllers/account.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ import (
"encoding/json"
"fmt"
"strconv"
"strings"

"github.com/astaxie/beego"
"github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/util"
)
Expand Down Expand Up @@ -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"`
Expand Down Expand Up @@ -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
Expand Down
22 changes: 22 additions & 0 deletions controllers/base.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion object/oidc_discovery.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"},
Expand Down
6 changes: 3 additions & 3 deletions object/token.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 = ""
}

Expand Down Expand Up @@ -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)
}
Expand Down
8 changes: 6 additions & 2 deletions object/token_jwt.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand All @@ -38,6 +39,7 @@ type UserShort struct {
type ClaimsShort struct {
*UserShort
Nonce string `json:"nonce,omitempty"`
Scope string `json:"scope,omitempty"`
jwt.RegisteredClaims
}

Expand All @@ -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)
Expand All @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions routers/auto_signin_filter.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down Expand Up @@ -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])
}
}
12 changes: 12 additions & 0 deletions routers/base.go
Original file line number Diff line number Diff line change
Expand Up @@ -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, " ")
Expand Down
1 change: 1 addition & 0 deletions routers/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down

0 comments on commit 0517523

Please sign in to comment.