From ab5af979c8b013d2d2556b571b77d61036f36e5f Mon Sep 17 00:00:00 2001 From: Leon Date: Thu, 3 Mar 2022 17:48:47 +0800 Subject: [PATCH] feat: add Oauth 2.0 Token Introspection(rfc7662) endpoint support (#532) Signed-off-by: Leon --- controllers/token.go | 62 ++++++++++++++++++++++++++++++++++++++++++++ object/token.go | 32 +++++++++++++++++++++++ object/token_jwt.go | 4 +++ routers/router.go | 1 + 4 files changed, 99 insertions(+) diff --git a/controllers/token.go b/controllers/token.go index 3e681474..fd0833b6 100644 --- a/controllers/token.go +++ b/controllers/token.go @@ -229,3 +229,65 @@ func (c *ApiController) TokenLogout() { c.Data["json"] = wrapActionResponse(flag) c.ServeJSON() } + +// IntrospectToken +// @Title IntrospectToken +// @Description The introspection endpoint is an OAuth 2.0 endpoint that takes a +// parameter representing an OAuth 2.0 token and returns a JSON document +// representing the meta information surrounding the +// token, including whether this token is currently active. +// This endpoint only support Basic Authorization. +// @Param body body {object.TokenIntrospectionRequest} true "the request body" +// @Success 200 {object} object.IntrospectionResponse The Response object +// @router /login/oauth/introspect [post] +func (c *ApiController) IntrospectToken() { + var body object.TokenIntrospectionRequest + err := json.Unmarshal(c.Ctx.Input.RequestBody, &body) + clientId, clientSecret, ok := c.Ctx.Request.BasicAuth() + if !ok { + util.LogWarning(c.Ctx, "Basic Authorization parses failed") + c.Data["json"] = Response{Status: "error", Msg: "Unauthorized operation"} + c.ServeJSON() + return + } + application := object.GetApplicationByClientId(clientId) + if application == nil || application.ClientSecret != clientSecret { + util.LogWarning(c.Ctx, "Basic Authorization failed") + c.Data["json"] = Response{Status: "error", Msg: "Unauthorized operation"} + c.ServeJSON() + return + } + token := object.GetTokenByTokenAndApplication(body.Token, application.Name) + if token == nil { + util.LogWarning(c.Ctx, "application: %s can not find token", application.Name) + c.Data["json"] = &object.IntrospectionResponse{Active: false} + c.ServeJSON() + return + } + jwtToken, err := object.ParseJwtTokenByApplication(body.Token, application) + if err != nil || jwtToken.Valid() != nil { + // and token revoked case. but we not implement + // TODO: 2022-03-03 add token revoked check, when we implemented the Token Revocation(rfc7009) Specs. + // refs: https://tools.ietf.org/html/rfc7009 + util.LogWarning(c.Ctx, "token invalid") + c.Data["json"] = &object.IntrospectionResponse{Active: false} + c.ServeJSON() + return + } + + c.Data["json"] = &object.IntrospectionResponse{ + Active: true, + Scope: jwtToken.Scope, + ClientId: clientId, + Username: token.User, + TokenType: token.TokenType, + Exp: jwtToken.ExpiresAt.Unix(), + Iat: jwtToken.IssuedAt.Unix(), + Nbf: jwtToken.NotBefore.Unix(), + Sub: jwtToken.Subject, + Aud: jwtToken.Audience, + Iss: jwtToken.Issuer, + Jti: jwtToken.Id, + } + c.ServeJSON() +} diff --git a/object/token.go b/object/token.go index 0c888a1e..3b81a93a 100644 --- a/object/token.go +++ b/object/token.go @@ -60,6 +60,29 @@ type TokenWrapper struct { Scope string `json:"scope"` } +type TokenIntrospectionRequest struct { + // access_token's value or refresh_token's value + Token string `json:"token"` + // pass this parameter to help the authorization server optimize the token lookup. + // value is one of `access_token` or `refresh_token` + TokenTypeHint string `json:"token_type_hint,omitempty"` +} + +type IntrospectionResponse struct { + Active bool `json:"active"` + Scope string `json:"scope,omitempty"` + ClientId string `json:"client_id,omitempty"` + Username string `json:"username,omitempty"` + TokenType string `json:"token_type,omitempty"` + Exp int64 `json:"exp,omitempty"` + Iat int64 `json:"iat,omitempty"` + Nbf int64 `json:"nbf,omitempty"` + Sub string `json:"sub,omitempty"` + Aud []string `json:"aud,omitempty"` + Iss string `json:"iss,omitempty"` + Jti string `json:"jti,omitempty"` +} + func GetTokenCount(owner, field, value string) int { session := GetSession(owner, -1, -1, field, value, "", "") count, err := session.Count(&Token{}) @@ -198,6 +221,15 @@ func GetTokenByAccessToken(accessToken string) *Token { return &token } +func GetTokenByTokenAndApplication(token string, application string) *Token { + tokenResult := Token{} + existed, err := adapter.Engine.Where("(refresh_token = ? or access_token = ? ) and application = ?", token, token, application).Get(&tokenResult) + if err != nil || !existed { + return nil + } + return &tokenResult +} + func CheckOAuthLogin(clientId string, responseType string, redirectUri string, scope string, state string) (string, *Application) { if responseType != "code" && responseType != "token" && responseType != "id_token" { return fmt.Sprintf("error: grant_type: %s is not supported in this application", responseType), nil diff --git a/object/token_jwt.go b/object/token_jwt.go index 14edcdfa..fc28dab8 100644 --- a/object/token_jwt.go +++ b/object/token_jwt.go @@ -147,3 +147,7 @@ func ParseJwtToken(token string, cert *Cert) (*Claims, error) { return nil, err } + +func ParseJwtTokenByApplication(token string, application *Application) (*Claims, error) { + return ParseJwtToken(token, getCertByApplication(application)) +} diff --git a/routers/router.go b/routers/router.go index e6b65225..3a5c6f2b 100644 --- a/routers/router.go +++ b/routers/router.go @@ -127,6 +127,7 @@ func initAPI() { beego.Router("/api/login/oauth/code", &controllers.ApiController{}, "POST:GetOAuthCode") beego.Router("/api/login/oauth/access_token", &controllers.ApiController{}, "POST:GetOAuthToken") beego.Router("/api/login/oauth/refresh_token", &controllers.ApiController{}, "POST:RefreshToken") + beego.Router("/api/login/oauth/introspect", &controllers.ApiController{}, "POST:IntrospectToken") beego.Router("/api/login/oauth/logout", &controllers.ApiController{}, "GET:TokenLogout") beego.Router("/api/get-records", &controllers.ApiController{}, "GET:GetRecords")