From 0bd58d61e547f482dd3c38a30fccb4c58caf2a67 Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Sat, 21 Aug 2021 04:16:45 +0200 Subject: [PATCH] Added introspection endpoint. (#16752) Co-authored-by: Lunny Xiao --- 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"