Skip to content

Commit 0bd58d6

Browse files
KN4CK3Rlunny
andauthored
Added introspection endpoint. (#16752)
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
1 parent e9747de commit 0bd58d6

File tree

5 files changed

+67
-56
lines changed

5 files changed

+67
-56
lines changed

routers/web/user/oauth.go

+43-55
Original file line numberDiff line numberDiff line change
@@ -96,24 +96,6 @@ func (err AccessTokenError) Error() string {
9696
return fmt.Sprintf("%s: %s", err.ErrorCode, err.ErrorDescription)
9797
}
9898

99-
// BearerTokenErrorCode represents an error code specified in RFC 6750
100-
type BearerTokenErrorCode string
101-
102-
const (
103-
// BearerTokenErrorCodeInvalidRequest represents an error code specified in RFC 6750
104-
BearerTokenErrorCodeInvalidRequest BearerTokenErrorCode = "invalid_request"
105-
// BearerTokenErrorCodeInvalidToken represents an error code specified in RFC 6750
106-
BearerTokenErrorCodeInvalidToken BearerTokenErrorCode = "invalid_token"
107-
// BearerTokenErrorCodeInsufficientScope represents an error code specified in RFC 6750
108-
BearerTokenErrorCodeInsufficientScope BearerTokenErrorCode = "insufficient_scope"
109-
)
110-
111-
// BearerTokenError represents an error response specified in RFC 6750
112-
type BearerTokenError struct {
113-
ErrorCode BearerTokenErrorCode `json:"error" form:"error"`
114-
ErrorDescription string `json:"error_description"`
115-
}
116-
11799
// TokenType specifies the kind of token
118100
type TokenType string
119101

@@ -253,35 +235,56 @@ type userInfoResponse struct {
253235

254236
// InfoOAuth manages request for userinfo endpoint
255237
func InfoOAuth(ctx *context.Context) {
256-
header := ctx.Req.Header.Get("Authorization")
257-
auths := strings.Fields(header)
258-
if len(auths) != 2 || auths[0] != "Bearer" {
259-
ctx.HandleText(http.StatusUnauthorized, "no valid auth token authorization")
260-
return
261-
}
262-
uid := auth.CheckOAuthAccessToken(auths[1])
263-
if uid == 0 {
264-
handleBearerTokenError(ctx, BearerTokenError{
265-
ErrorCode: BearerTokenErrorCodeInvalidToken,
266-
ErrorDescription: "Access token not assigned to any user",
267-
})
268-
return
269-
}
270-
authUser, err := models.GetUserByID(uid)
271-
if err != nil {
272-
ctx.ServerError("GetUserByID", err)
238+
if ctx.User == nil || ctx.Data["AuthedMethod"] != (&auth.OAuth2{}).Name() {
239+
ctx.Resp.Header().Set("WWW-Authenticate", `Bearer realm=""`)
240+
ctx.HandleText(http.StatusUnauthorized, "no valid authorization")
273241
return
274242
}
275243
response := &userInfoResponse{
276-
Sub: fmt.Sprint(authUser.ID),
277-
Name: authUser.FullName,
278-
Username: authUser.Name,
279-
Email: authUser.Email,
280-
Picture: authUser.AvatarLink(),
244+
Sub: fmt.Sprint(ctx.User.ID),
245+
Name: ctx.User.FullName,
246+
Username: ctx.User.Name,
247+
Email: ctx.User.Email,
248+
Picture: ctx.User.AvatarLink(),
281249
}
282250
ctx.JSON(http.StatusOK, response)
283251
}
284252

253+
// IntrospectOAuth introspects an oauth token
254+
func IntrospectOAuth(ctx *context.Context) {
255+
if ctx.User == nil {
256+
ctx.Resp.Header().Set("WWW-Authenticate", `Bearer realm=""`)
257+
ctx.HandleText(http.StatusUnauthorized, "no valid authorization")
258+
return
259+
}
260+
261+
var response struct {
262+
Active bool `json:"active"`
263+
Scope string `json:"scope,omitempty"`
264+
jwt.StandardClaims
265+
}
266+
267+
form := web.GetForm(ctx).(*forms.IntrospectTokenForm)
268+
token, err := oauth2.ParseToken(form.Token)
269+
if err == nil {
270+
if token.Valid() == nil {
271+
grant, err := models.GetOAuth2GrantByID(token.GrantID)
272+
if err == nil && grant != nil {
273+
app, err := models.GetOAuth2ApplicationByID(grant.ApplicationID)
274+
if err == nil && app != nil {
275+
response.Active = true
276+
response.Scope = grant.Scope
277+
response.Issuer = setting.AppURL
278+
response.Audience = app.ClientID
279+
response.Subject = fmt.Sprint(grant.UserID)
280+
}
281+
}
282+
}
283+
}
284+
285+
ctx.JSON(http.StatusOK, response)
286+
}
287+
285288
// AuthorizeOAuth manages authorize requests
286289
func AuthorizeOAuth(ctx *context.Context) {
287290
form := web.GetForm(ctx).(*forms.AuthorizationForm)
@@ -697,18 +700,3 @@ func handleAuthorizeError(ctx *context.Context, authErr AuthorizeError, redirect
697700
redirect.RawQuery = q.Encode()
698701
ctx.Redirect(redirect.String(), 302)
699702
}
700-
701-
func handleBearerTokenError(ctx *context.Context, beErr BearerTokenError) {
702-
ctx.Resp.Header().Set("WWW-Authenticate", fmt.Sprintf("Bearer realm=\"\", error=\"%s\", error_description=\"%s\"", beErr.ErrorCode, beErr.ErrorDescription))
703-
switch beErr.ErrorCode {
704-
case BearerTokenErrorCodeInvalidRequest:
705-
ctx.JSON(http.StatusBadRequest, beErr)
706-
case BearerTokenErrorCodeInvalidToken:
707-
ctx.JSON(http.StatusUnauthorized, beErr)
708-
case BearerTokenErrorCodeInsufficientScope:
709-
ctx.JSON(http.StatusForbidden, beErr)
710-
default:
711-
log.Error("Invalid BearerTokenErrorCode: %v", beErr.ErrorCode)
712-
ctx.ServerError("Unhandled BearerTokenError", fmt.Errorf("BearerTokenError: error=\"%v\", error_description=\"%v\"", beErr.ErrorCode, beErr.ErrorDescription))
713-
}
714-
}

routers/web/web.go

+1
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,7 @@ func RegisterRoutes(m *web.Route) {
311311
m.Get("/login/oauth/userinfo", ignSignInAndCsrf, user.InfoOAuth)
312312
m.Post("/login/oauth/access_token", CorsHandler(), bindIgnErr(forms.AccessTokenForm{}), ignSignInAndCsrf, user.AccessTokenOAuth)
313313
m.Get("/login/oauth/keys", ignSignInAndCsrf, user.OIDCKeys)
314+
m.Post("/login/oauth/introspect", CorsHandler(), bindIgnErr(forms.IntrospectTokenForm{}), ignSignInAndCsrf, user.IntrospectOAuth)
314315

315316
m.Group("/user/settings", func() {
316317
m.Get("", userSetting.Profile)

services/auth/oauth2.go

+11-1
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ func (o *OAuth2) Verify(req *http.Request, w http.ResponseWriter, store DataStor
113113
return nil
114114
}
115115

116-
if !middleware.IsAPIPath(req) && !isAttachmentDownload(req) {
116+
if !middleware.IsAPIPath(req) && !isAttachmentDownload(req) && !isAuthenticatedTokenRequest(req) {
117117
return nil
118118
}
119119

@@ -134,3 +134,13 @@ func (o *OAuth2) Verify(req *http.Request, w http.ResponseWriter, store DataStor
134134
log.Trace("OAuth2 Authorization: Logged in user %-v", user)
135135
return user
136136
}
137+
138+
func isAuthenticatedTokenRequest(req *http.Request) bool {
139+
switch req.URL.Path {
140+
case "/login/oauth/userinfo":
141+
fallthrough
142+
case "/login/oauth/introspect":
143+
return true
144+
}
145+
return false
146+
}

services/forms/user_form.go

+11
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,17 @@ func (f *AccessTokenForm) Validate(req *http.Request, errs binding.Errors) bindi
215215
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
216216
}
217217

218+
// IntrospectTokenForm for introspecting tokens
219+
type IntrospectTokenForm struct {
220+
Token string `json:"token"`
221+
}
222+
223+
// Validate validates the fields
224+
func (f *IntrospectTokenForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
225+
ctx := context.GetContext(req)
226+
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
227+
}
228+
218229
// __________________________________________.___ _______ ________ _________
219230
// / _____/\_ _____/\__ ___/\__ ___/| |\ \ / _____/ / _____/
220231
// \_____ \ | __)_ | | | | | |/ | \/ \ ___ \_____ \

templates/user/auth/oidc_wellknown.tmpl

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"token_endpoint": "{{AppUrl | JSEscape | Safe}}login/oauth/access_token",
55
"jwks_uri": "{{AppUrl | JSEscape | Safe}}login/oauth/keys",
66
"userinfo_endpoint": "{{AppUrl | JSEscape | Safe}}login/oauth/userinfo",
7+
"introspection_endpoint": "{{AppUrl | JSEscape | Safe}}login/oauth/introspect",
78
"response_types_supported": [
89
"code",
910
"id_token"

0 commit comments

Comments
 (0)