mirror of
				https://gitee.com/gitea/gitea
				synced 2025-11-04 16:40:24 +08:00 
			
		
		
		
	Added introspection endpoint. (#16752)
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
This commit is contained in:
		@@ -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))
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
//   __________________________________________.___ _______    ________  _________
 | 
			
		||||
//  /   _____/\_   _____/\__    ___/\__    ___/|   |\      \  /  _____/ /   _____/
 | 
			
		||||
//  \_____  \  |    __)_   |    |     |    |   |   |/   |   \/   \  ___ \_____  \
 | 
			
		||||
 
 | 
			
		||||
@@ -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"
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user