Browse Source

Added introspection endpoint. (#16752)

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
tags/v1.16.0-rc1
KN4CK3R 2 years ago
parent
commit
0bd58d61e5
No account linked to committer's email address

+ 43
- 55
routers/web/user/oauth.go View File

@@ -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))
}
}

+ 1
- 0
routers/web/web.go View File

@@ -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)

+ 11
- 1
services/auth/oauth2.go View File

@@ -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
}

+ 11
- 0
services/forms/user_form.go 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)
}

// 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)
}

// __________________________________________.___ _______ ________ _________
// / _____/\_ _____/\__ ___/\__ ___/| |\ \ / _____/ / _____/
// \_____ \ | __)_ | | | | | |/ | \/ \ ___ \_____ \

+ 1
- 0
templates/user/auth/oidc_wellknown.tmpl View File

@@ -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"

Loading…
Cancel
Save