Added introspection endpoint. (#16752)

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
This commit is contained in:
KN4CK3R 2021-08-21 04:16:45 +02:00 committed by GitHub
parent e9747de952
commit 0bd58d61e5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 67 additions and 56 deletions

View file

@ -96,24 +96,6 @@ func (err AccessTokenError) Error() string {
return fmt.Sprintf("%s: %s", err.ErrorCode, err.ErrorDescription) 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 // TokenType specifies the kind of token
type TokenType string type TokenType string
@ -253,35 +235,56 @@ type userInfoResponse struct {
// InfoOAuth manages request for userinfo endpoint // InfoOAuth manages request for userinfo endpoint
func InfoOAuth(ctx *context.Context) { func InfoOAuth(ctx *context.Context) {
header := ctx.Req.Header.Get("Authorization") if ctx.User == nil || ctx.Data["AuthedMethod"] != (&auth.OAuth2{}).Name() {
auths := strings.Fields(header) ctx.Resp.Header().Set("WWW-Authenticate", `Bearer realm=""`)
if len(auths) != 2 || auths[0] != "Bearer" { ctx.HandleText(http.StatusUnauthorized, "no valid authorization")
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)
return return
} }
response := &userInfoResponse{ response := &userInfoResponse{
Sub: fmt.Sprint(authUser.ID), Sub: fmt.Sprint(ctx.User.ID),
Name: authUser.FullName, Name: ctx.User.FullName,
Username: authUser.Name, Username: ctx.User.Name,
Email: authUser.Email, Email: ctx.User.Email,
Picture: authUser.AvatarLink(), Picture: ctx.User.AvatarLink(),
} }
ctx.JSON(http.StatusOK, response) 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 // AuthorizeOAuth manages authorize requests
func AuthorizeOAuth(ctx *context.Context) { func AuthorizeOAuth(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.AuthorizationForm) form := web.GetForm(ctx).(*forms.AuthorizationForm)
@ -697,18 +700,3 @@ func handleAuthorizeError(ctx *context.Context, authErr AuthorizeError, redirect
redirect.RawQuery = q.Encode() redirect.RawQuery = q.Encode()
ctx.Redirect(redirect.String(), 302) 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))
}
}

View file

@ -311,6 +311,7 @@ func RegisterRoutes(m *web.Route) {
m.Get("/login/oauth/userinfo", ignSignInAndCsrf, user.InfoOAuth) m.Get("/login/oauth/userinfo", ignSignInAndCsrf, user.InfoOAuth)
m.Post("/login/oauth/access_token", CorsHandler(), bindIgnErr(forms.AccessTokenForm{}), ignSignInAndCsrf, user.AccessTokenOAuth) m.Post("/login/oauth/access_token", CorsHandler(), bindIgnErr(forms.AccessTokenForm{}), ignSignInAndCsrf, user.AccessTokenOAuth)
m.Get("/login/oauth/keys", ignSignInAndCsrf, user.OIDCKeys) 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.Group("/user/settings", func() {
m.Get("", userSetting.Profile) m.Get("", userSetting.Profile)

View file

@ -113,7 +113,7 @@ func (o *OAuth2) Verify(req *http.Request, w http.ResponseWriter, store DataStor
return nil return nil
} }
if !middleware.IsAPIPath(req) && !isAttachmentDownload(req) { if !middleware.IsAPIPath(req) && !isAttachmentDownload(req) && !isAuthenticatedTokenRequest(req) {
return nil 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) log.Trace("OAuth2 Authorization: Logged in user %-v", user)
return 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
}

View file

@ -215,6 +215,17 @@ func (f *AccessTokenForm) Validate(req *http.Request, errs binding.Errors) bindi
return middleware.Validate(errs, ctx.Data, f, ctx.Locale) 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)
}
// __________________________________________.___ _______ ________ _________ // __________________________________________.___ _______ ________ _________
// / _____/\_ _____/\__ ___/\__ ___/| |\ \ / _____/ / _____/ // / _____/\_ _____/\__ ___/\__ ___/| |\ \ / _____/ / _____/
// \_____ \ | __)_ | | | | | |/ | \/ \ ___ \_____ \ // \_____ \ | __)_ | | | | | |/ | \/ \ ___ \_____ \

View file

@ -4,6 +4,7 @@
"token_endpoint": "{{AppUrl | JSEscape | Safe}}login/oauth/access_token", "token_endpoint": "{{AppUrl | JSEscape | Safe}}login/oauth/access_token",
"jwks_uri": "{{AppUrl | JSEscape | Safe}}login/oauth/keys", "jwks_uri": "{{AppUrl | JSEscape | Safe}}login/oauth/keys",
"userinfo_endpoint": "{{AppUrl | JSEscape | Safe}}login/oauth/userinfo", "userinfo_endpoint": "{{AppUrl | JSEscape | Safe}}login/oauth/userinfo",
"introspection_endpoint": "{{AppUrl | JSEscape | Safe}}login/oauth/introspect",
"response_types_supported": [ "response_types_supported": [
"code", "code",
"id_token" "id_token"