feat: add userinfo endpoint (#447)

* feat: add userinfo endpoint

Signed-off-by: 0x2a <stevesough@gmail.com>

* feat: add scope support

Signed-off-by: 0x2a <stevesough@gmail.com>

* fix: modify the endpoint of discovery

Signed-off-by: 0x2a <stevesough@gmail.com>
This commit is contained in:
Steve0x2a
2022-01-26 11:56:01 +08:00
committed by GitHub
parent c87c001da3
commit 051752340d
9 changed files with 103 additions and 6 deletions

View File

@ -80,6 +80,7 @@ p, *, *, POST, /api/login, *, *
p, *, *, GET, /api/get-app-login, *, * p, *, *, GET, /api/get-app-login, *, *
p, *, *, POST, /api/logout, *, * p, *, *, POST, /api/logout, *, *
p, *, *, GET, /api/get-account, *, * p, *, *, GET, /api/get-account, *, *
p, *, *, GET, /api/userinfo, *, *
p, *, *, POST, /api/login/oauth/access_token, *, * p, *, *, POST, /api/login/oauth/access_token, *, *
p, *, *, POST, /api/login/oauth/refresh_token, *, * p, *, *, POST, /api/login/oauth/refresh_token, *, *
p, *, *, GET, /api/get-application, *, * p, *, *, GET, /api/get-application, *, *

View File

@ -18,7 +18,9 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"strconv" "strconv"
"strings"
"github.com/astaxie/beego"
"github.com/casdoor/casdoor/object" "github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/util" "github.com/casdoor/casdoor/util"
) )
@ -67,6 +69,18 @@ type Response struct {
Data2 interface{} `json:"data2"` 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 HumanCheck struct {
Type string `json:"type"` Type string `json:"type"`
AppKey string `json:"appKey"` AppKey string `json:"appKey"`
@ -231,6 +245,47 @@ func (c *ApiController) GetAccount() {
c.ServeJSON() 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 ... // GetHumanCheck ...
// @Tag Login API // @Tag Login API
// @Title GetHumancheck // @Title GetHumancheck

View File

@ -72,6 +72,28 @@ func (c *ApiController) GetSessionUsername() string {
return user.(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 ... // SetSessionUsername ...
func (c *ApiController) SetSessionUsername(user string) { func (c *ApiController) SetSessionUsername(user string) {
c.SetSession("username", user) c.SetSession("username", user)

View File

@ -54,7 +54,7 @@ func init() {
Issuer: origin, Issuer: origin,
AuthorizationEndpoint: fmt.Sprintf("%s/login/oauth/authorize", origin), AuthorizationEndpoint: fmt.Sprintf("%s/login/oauth/authorize", origin),
TokenEndpoint: fmt.Sprintf("%s/api/login/oauth/access_token", 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), JwksUri: fmt.Sprintf("%s/api/certs", origin),
ResponseTypesSupported: []string{"id_token"}, ResponseTypesSupported: []string{"id_token"},
ResponseModesSupported: []string{"login", "code", "link"}, ResponseModesSupported: []string{"login", "code", "link"},

View File

@ -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 { if err != nil {
panic(err) panic(err)
} }
if challenge == "null"{ if challenge == "null" {
challenge = "" challenge = ""
} }
@ -376,7 +376,7 @@ func RefreshToken(grantType string, refreshToken string, scope string, clientId
Scope: "", Scope: "",
} }
} }
newAccessToken, newRefreshToken, err := generateJwtToken(application, user, "") newAccessToken, newRefreshToken, err := generateJwtToken(application, user, "", scope)
if err != nil { if err != nil {
panic(err) panic(err)
} }

View File

@ -27,6 +27,7 @@ type Claims struct {
*User *User
Nonce string `json:"nonce,omitempty"` Nonce string `json:"nonce,omitempty"`
Tag string `json:"tag,omitempty"` Tag string `json:"tag,omitempty"`
Scope string `json:"scope,omitempty"`
jwt.RegisteredClaims jwt.RegisteredClaims
} }
@ -38,6 +39,7 @@ type UserShort struct {
type ClaimsShort struct { type ClaimsShort struct {
*UserShort *UserShort
Nonce string `json:"nonce,omitempty"` Nonce string `json:"nonce,omitempty"`
Scope string `json:"scope,omitempty"`
jwt.RegisteredClaims jwt.RegisteredClaims
} }
@ -53,12 +55,13 @@ func getShortClaims(claims Claims) ClaimsShort {
res := ClaimsShort{ res := ClaimsShort{
UserShort: getShortUser(claims.User), UserShort: getShortUser(claims.User),
Nonce: claims.Nonce, Nonce: claims.Nonce,
Scope: claims.Scope,
RegisteredClaims: claims.RegisteredClaims, RegisteredClaims: claims.RegisteredClaims,
} }
return res 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() nowTime := time.Now()
expireTime := nowTime.Add(time.Duration(application.ExpireInHours) * time.Hour) expireTime := nowTime.Add(time.Duration(application.ExpireInHours) * time.Hour)
refreshExpireTime := nowTime.Add(time.Duration(application.RefreshExpireInHours) * time.Hour) refreshExpireTime := nowTime.Add(time.Duration(application.RefreshExpireInHours) * time.Hour)
@ -70,6 +73,7 @@ func generateJwtToken(application *Application, user *User, nonce string) (strin
Nonce: nonce, Nonce: nonce,
// FIXME: A workaround for custom claim by reusing `tag` in user info // FIXME: A workaround for custom claim by reusing `tag` in user info
Tag: user.Tag, Tag: user.Tag,
Scope: scope,
RegisteredClaims: jwt.RegisteredClaims{ RegisteredClaims: jwt.RegisteredClaims{
Issuer: beego.AppConfig.String("origin"), Issuer: beego.AppConfig.String("origin"),
Subject: user.Id, Subject: user.Id,

View File

@ -43,6 +43,7 @@ func AutoSigninFilter(ctx *context.Context) {
userId := fmt.Sprintf("%s/%s", claims.User.Owner, claims.User.Name) userId := fmt.Sprintf("%s/%s", claims.User.Owner, claims.User.Name)
setSessionUser(ctx, userId) setSessionUser(ctx, userId)
setSessionOidc(ctx, claims.Scope, claims.Audience[0])
return return
} }
@ -81,5 +82,6 @@ func AutoSigninFilter(ctx *context.Context) {
setSessionUser(ctx, fmt.Sprintf("%s/%s", claims.Owner, claims.Name)) setSessionUser(ctx, fmt.Sprintf("%s/%s", claims.Owner, claims.Name))
setSessionExpire(ctx, claims.ExpiresAt.Unix()) setSessionExpire(ctx, claims.ExpiresAt.Unix())
setSessionOidc(ctx, claims.Scope, claims.Audience[0])
} }
} }

View File

@ -97,6 +97,18 @@ func setSessionExpire(ctx *context.Context, ExpireTime int64) {
ctx.Input.CruSession.SessionRelease(ctx.ResponseWriter) 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 { func parseBearerToken(ctx *context.Context) string {
header := ctx.Request.Header.Get("Authorization") header := ctx.Request.Header.Get("Authorization")
tokens := strings.Split(header, " ") tokens := strings.Split(header, " ")

View File

@ -50,6 +50,7 @@ func initAPI() {
beego.Router("/api/get-app-login", &controllers.ApiController{}, "GET:GetApplicationLogin") beego.Router("/api/get-app-login", &controllers.ApiController{}, "GET:GetApplicationLogin")
beego.Router("/api/logout", &controllers.ApiController{}, "POST:Logout") beego.Router("/api/logout", &controllers.ApiController{}, "POST:Logout")
beego.Router("/api/get-account", &controllers.ApiController{}, "GET:GetAccount") 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/unlink", &controllers.ApiController{}, "POST:Unlink")
beego.Router("/api/get-saml-login", &controllers.ApiController{}, "GET:GetSamlLogin") beego.Router("/api/get-saml-login", &controllers.ApiController{}, "GET:GetSamlLogin")
beego.Router("/api/acs", &controllers.ApiController{}, "POST:HandleSamlLogin") beego.Router("/api/acs", &controllers.ApiController{}, "POST:HandleSamlLogin")