From 0bd58d61e547f482dd3c38a30fccb4c58caf2a67 Mon Sep 17 00:00:00 2001
From: KN4CK3R <admin@oldschoolhack.me>
Date: Sat, 21 Aug 2021 04:16:45 +0200
Subject: [PATCH] Added introspection endpoint. (#16752)

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
---
 routers/web/user/oauth.go               | 98 +++++++++++--------------
 routers/web/web.go                      |  1 +
 services/auth/oauth2.go                 | 12 ++-
 services/forms/user_form.go             | 11 +++
 templates/user/auth/oidc_wellknown.tmpl |  1 +
 5 files changed, 67 insertions(+), 56 deletions(-)

diff --git a/routers/web/user/oauth.go b/routers/web/user/oauth.go
index 67e4ea0622..771bd90b15 100644
--- a/routers/web/user/oauth.go
+++ b/routers/web/user/oauth.go
@@ -96,24 +96,6 @@ func (err AccessTokenError) Error() string {
 	return fmt.Sprintf("%s: %s", err.ErrorCode, err.ErrorDescription)
 }
 
-// BearerTokenErrorCode represents an error code specified in RFC 6750
-type BearerTokenErrorCode string
-
-const (
-	// BearerTokenErrorCodeInvalidRequest represents an error code specified in RFC 6750
-	BearerTokenErrorCodeInvalidRequest BearerTokenErrorCode = "invalid_request"
-	// BearerTokenErrorCodeInvalidToken represents an error code specified in RFC 6750
-	BearerTokenErrorCodeInvalidToken BearerTokenErrorCode = "invalid_token"
-	// BearerTokenErrorCodeInsufficientScope represents an error code specified in RFC 6750
-	BearerTokenErrorCodeInsufficientScope BearerTokenErrorCode = "insufficient_scope"
-)
-
-// BearerTokenError represents an error response specified in RFC 6750
-type BearerTokenError struct {
-	ErrorCode        BearerTokenErrorCode `json:"error" form:"error"`
-	ErrorDescription string               `json:"error_description"`
-}
-
 // TokenType specifies the kind of token
 type TokenType string
 
@@ -253,35 +235,56 @@ type userInfoResponse struct {
 
 // InfoOAuth manages request for userinfo endpoint
 func InfoOAuth(ctx *context.Context) {
-	header := ctx.Req.Header.Get("Authorization")
-	auths := strings.Fields(header)
-	if len(auths) != 2 || auths[0] != "Bearer" {
-		ctx.HandleText(http.StatusUnauthorized, "no valid auth token authorization")
-		return
-	}
-	uid := auth.CheckOAuthAccessToken(auths[1])
-	if uid == 0 {
-		handleBearerTokenError(ctx, BearerTokenError{
-			ErrorCode:        BearerTokenErrorCodeInvalidToken,
-			ErrorDescription: "Access token not assigned to any user",
-		})
-		return
-	}
-	authUser, err := models.GetUserByID(uid)
-	if err != nil {
-		ctx.ServerError("GetUserByID", err)
+	if ctx.User == nil || ctx.Data["AuthedMethod"] != (&auth.OAuth2{}).Name() {
+		ctx.Resp.Header().Set("WWW-Authenticate", `Bearer realm=""`)
+		ctx.HandleText(http.StatusUnauthorized, "no valid authorization")
 		return
 	}
 	response := &userInfoResponse{
-		Sub:      fmt.Sprint(authUser.ID),
-		Name:     authUser.FullName,
-		Username: authUser.Name,
-		Email:    authUser.Email,
-		Picture:  authUser.AvatarLink(),
+		Sub:      fmt.Sprint(ctx.User.ID),
+		Name:     ctx.User.FullName,
+		Username: ctx.User.Name,
+		Email:    ctx.User.Email,
+		Picture:  ctx.User.AvatarLink(),
 	}
 	ctx.JSON(http.StatusOK, response)
 }
 
+// IntrospectOAuth introspects an oauth token
+func IntrospectOAuth(ctx *context.Context) {
+	if ctx.User == nil {
+		ctx.Resp.Header().Set("WWW-Authenticate", `Bearer realm=""`)
+		ctx.HandleText(http.StatusUnauthorized, "no valid authorization")
+		return
+	}
+
+	var response struct {
+		Active bool   `json:"active"`
+		Scope  string `json:"scope,omitempty"`
+		jwt.StandardClaims
+	}
+
+	form := web.GetForm(ctx).(*forms.IntrospectTokenForm)
+	token, err := oauth2.ParseToken(form.Token)
+	if err == nil {
+		if token.Valid() == nil {
+			grant, err := models.GetOAuth2GrantByID(token.GrantID)
+			if err == nil && grant != nil {
+				app, err := models.GetOAuth2ApplicationByID(grant.ApplicationID)
+				if err == nil && app != nil {
+					response.Active = true
+					response.Scope = grant.Scope
+					response.Issuer = setting.AppURL
+					response.Audience = app.ClientID
+					response.Subject = fmt.Sprint(grant.UserID)
+				}
+			}
+		}
+	}
+
+	ctx.JSON(http.StatusOK, response)
+}
+
 // AuthorizeOAuth manages authorize requests
 func AuthorizeOAuth(ctx *context.Context) {
 	form := web.GetForm(ctx).(*forms.AuthorizationForm)
@@ -697,18 +700,3 @@ func handleAuthorizeError(ctx *context.Context, authErr AuthorizeError, redirect
 	redirect.RawQuery = q.Encode()
 	ctx.Redirect(redirect.String(), 302)
 }
-
-func handleBearerTokenError(ctx *context.Context, beErr BearerTokenError) {
-	ctx.Resp.Header().Set("WWW-Authenticate", fmt.Sprintf("Bearer realm=\"\", error=\"%s\", error_description=\"%s\"", beErr.ErrorCode, beErr.ErrorDescription))
-	switch beErr.ErrorCode {
-	case BearerTokenErrorCodeInvalidRequest:
-		ctx.JSON(http.StatusBadRequest, beErr)
-	case BearerTokenErrorCodeInvalidToken:
-		ctx.JSON(http.StatusUnauthorized, beErr)
-	case BearerTokenErrorCodeInsufficientScope:
-		ctx.JSON(http.StatusForbidden, beErr)
-	default:
-		log.Error("Invalid BearerTokenErrorCode: %v", beErr.ErrorCode)
-		ctx.ServerError("Unhandled BearerTokenError", fmt.Errorf("BearerTokenError: error=\"%v\", error_description=\"%v\"", beErr.ErrorCode, beErr.ErrorDescription))
-	}
-}
diff --git a/routers/web/web.go b/routers/web/web.go
index a47fd518ac..98395578f6 100644
--- a/routers/web/web.go
+++ b/routers/web/web.go
@@ -311,6 +311,7 @@ func RegisterRoutes(m *web.Route) {
 	m.Get("/login/oauth/userinfo", ignSignInAndCsrf, user.InfoOAuth)
 	m.Post("/login/oauth/access_token", CorsHandler(), bindIgnErr(forms.AccessTokenForm{}), ignSignInAndCsrf, user.AccessTokenOAuth)
 	m.Get("/login/oauth/keys", ignSignInAndCsrf, user.OIDCKeys)
+	m.Post("/login/oauth/introspect", CorsHandler(), bindIgnErr(forms.IntrospectTokenForm{}), ignSignInAndCsrf, user.IntrospectOAuth)
 
 	m.Group("/user/settings", func() {
 		m.Get("", userSetting.Profile)
diff --git a/services/auth/oauth2.go b/services/auth/oauth2.go
index 93806c7072..f7f870dade 100644
--- a/services/auth/oauth2.go
+++ b/services/auth/oauth2.go
@@ -113,7 +113,7 @@ func (o *OAuth2) Verify(req *http.Request, w http.ResponseWriter, store DataStor
 		return nil
 	}
 
-	if !middleware.IsAPIPath(req) && !isAttachmentDownload(req) {
+	if !middleware.IsAPIPath(req) && !isAttachmentDownload(req) && !isAuthenticatedTokenRequest(req) {
 		return nil
 	}
 
@@ -134,3 +134,13 @@ func (o *OAuth2) Verify(req *http.Request, w http.ResponseWriter, store DataStor
 	log.Trace("OAuth2 Authorization: Logged in user %-v", user)
 	return user
 }
+
+func isAuthenticatedTokenRequest(req *http.Request) bool {
+	switch req.URL.Path {
+	case "/login/oauth/userinfo":
+		fallthrough
+	case "/login/oauth/introspect":
+		return true
+	}
+	return false
+}
diff --git a/services/forms/user_form.go b/services/forms/user_form.go
index 1e12795c70..7d6b976936 100644
--- a/services/forms/user_form.go
+++ b/services/forms/user_form.go
@@ -215,6 +215,17 @@ func (f *AccessTokenForm) Validate(req *http.Request, errs binding.Errors) bindi
 	return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
 }
 
+// IntrospectTokenForm for introspecting tokens
+type IntrospectTokenForm struct {
+	Token string `json:"token"`
+}
+
+// Validate validates the fields
+func (f *IntrospectTokenForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
+	ctx := context.GetContext(req)
+	return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
+}
+
 //   __________________________________________.___ _______    ________  _________
 //  /   _____/\_   _____/\__    ___/\__    ___/|   |\      \  /  _____/ /   _____/
 //  \_____  \  |    __)_   |    |     |    |   |   |/   |   \/   \  ___ \_____  \
diff --git a/templates/user/auth/oidc_wellknown.tmpl b/templates/user/auth/oidc_wellknown.tmpl
index 93a048b513..d4cbf7dfec 100644
--- a/templates/user/auth/oidc_wellknown.tmpl
+++ b/templates/user/auth/oidc_wellknown.tmpl
@@ -4,6 +4,7 @@
     "token_endpoint": "{{AppUrl | JSEscape | Safe}}login/oauth/access_token",
     "jwks_uri": "{{AppUrl | JSEscape | Safe}}login/oauth/keys",
     "userinfo_endpoint": "{{AppUrl | JSEscape | Safe}}login/oauth/userinfo",
+    "introspection_endpoint": "{{AppUrl | JSEscape | Safe}}login/oauth/introspect",
     "response_types_supported": [
         "code",
         "id_token"