forked from gitea/gitea
		
	Added introspection endpoint. (#16752)
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
This commit is contained in:
		
							parent
							
								
									e9747de952
								
							
						
					
					
						commit
						0bd58d61e5
					
				| @ -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" | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 KN4CK3R
						KN4CK3R