feat(api): add token introspection and self-deletion endpoint (#37995)
Adds a /api/v1/token endpoint that allows tokens to introspect and delete themselves. partially fixes: https://github.com/go-gitea/gitea/issues/33583 Assisted-by: Mistral Vibe:mistral-medium-3.5 --------- Signed-off-by: wxiaoguang <wxiaoguang@gmail.com> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
@@ -88,6 +88,7 @@ import (
|
||||
"gitea.dev/routers/api/v1/packages"
|
||||
"gitea.dev/routers/api/v1/repo"
|
||||
"gitea.dev/routers/api/v1/settings"
|
||||
"gitea.dev/routers/api/v1/token"
|
||||
"gitea.dev/routers/api/v1/user"
|
||||
"gitea.dev/routers/common"
|
||||
"gitea.dev/services/actions"
|
||||
@@ -976,6 +977,11 @@ func Routes() *web.Router {
|
||||
})
|
||||
})
|
||||
|
||||
// Token introspection and deletion endpoint
|
||||
m.Combo("/token").
|
||||
Get(reqToken(), token.GetCurrentToken).
|
||||
Delete(reqToken(), token.DeleteCurrentToken)
|
||||
|
||||
// Notifications (requires 'notifications' scope)
|
||||
// The notifications API is not available for public-only tokens because a user's notifications mix
|
||||
// public and private repository events in the same mailbox.
|
||||
|
||||
@@ -20,3 +20,10 @@ type swaggerResponseAccessToken struct {
|
||||
// in:body
|
||||
Body api.AccessToken `json:"body"`
|
||||
}
|
||||
|
||||
// CurrentAccessToken represents the currently authenticated access token.
|
||||
// swagger:response CurrentAccessToken
|
||||
type swaggerResponseCurrentAccessToken struct {
|
||||
// in:body
|
||||
Body api.CurrentAccessToken `json:"body"`
|
||||
}
|
||||
|
||||
88
routers/api/v1/token/token.go
Normal file
88
routers/api/v1/token/token.go
Normal file
@@ -0,0 +1,88 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package token
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
auth_model "gitea.dev/models/auth"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/auth/httpauth"
|
||||
api "gitea.dev/modules/structs"
|
||||
"gitea.dev/modules/util"
|
||||
"gitea.dev/services/context"
|
||||
)
|
||||
|
||||
// GetCurrentToken returns metadata about the currently authenticated token.
|
||||
func GetCurrentToken(ctx *context.APIContext) {
|
||||
// swagger:operation GET /token miscellaneous getCurrentToken
|
||||
// ---
|
||||
// summary: Get the currently authenticated token
|
||||
// produces:
|
||||
// - application/json
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/CurrentAccessToken"
|
||||
accessToken, err := getToken(ctx)
|
||||
if err != nil {
|
||||
ctx.APIErrorAuto(err)
|
||||
return
|
||||
}
|
||||
|
||||
// Get user info
|
||||
user, err := user_model.GetUserByID(ctx, accessToken.UID)
|
||||
if err != nil {
|
||||
ctx.APIErrorAuto(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, &api.CurrentAccessToken{
|
||||
ID: accessToken.ID,
|
||||
Name: accessToken.Name,
|
||||
Scopes: accessToken.Scope.StringSlice(),
|
||||
CreatedAt: accessToken.CreatedUnix.AsTime(),
|
||||
LastUsedAt: accessToken.UpdatedUnix.AsTime(),
|
||||
User: &api.UserMeta{
|
||||
ID: user.ID,
|
||||
Login: user.Name,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteCurrentToken deletes the currently authenticated token.
|
||||
func DeleteCurrentToken(ctx *context.APIContext) {
|
||||
// swagger:operation DELETE /token miscellaneous deleteCurrentToken
|
||||
// ---
|
||||
// summary: Delete the currently authenticated token
|
||||
// produces:
|
||||
// - application/json
|
||||
// responses:
|
||||
// "204":
|
||||
// description: token deleted
|
||||
accessToken, err := getToken(ctx)
|
||||
if err != nil {
|
||||
ctx.APIErrorAuto(err)
|
||||
return
|
||||
}
|
||||
|
||||
// Delete the token
|
||||
err = auth_model.DeleteAccessTokenByID(ctx, accessToken.ID, accessToken.UID)
|
||||
if err != nil && !errors.Is(err, util.ErrNotExist) {
|
||||
ctx.APIErrorAuto(err)
|
||||
return
|
||||
}
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// getToken retrieves an access token from the API context's Authorization header and validates it against the database.
|
||||
// Returns nil if the token is invalid and handles the response
|
||||
func getToken(ctx *context.APIContext) (*auth_model.AccessToken, error) {
|
||||
authHeader := ctx.Req.Header.Get("Authorization")
|
||||
parsed, ok := httpauth.ParseAuthorizationHeader(authHeader)
|
||||
if !ok || parsed.BearerToken == nil {
|
||||
return nil, util.NewNotExistErrorf("invalid access token")
|
||||
}
|
||||
return auth_model.GetAccessTokenBySHA(ctx, parsed.BearerToken.Token)
|
||||
}
|
||||
@@ -191,17 +191,9 @@ func DeleteAccessToken(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
}
|
||||
if tokenID == 0 {
|
||||
ctx.APIErrorInternal(nil)
|
||||
return
|
||||
}
|
||||
|
||||
if err := auth_model.DeleteAccessTokenByID(ctx, tokenID, ctx.ContextUser.ID); err != nil {
|
||||
if auth_model.IsErrAccessTokenNotExist(err) {
|
||||
ctx.APIErrorNotFound()
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
ctx.APIErrorAuto(err)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user