feat(audit): emit the login event for the OAuth code exchange

The new v2 OAuth token endpoint mints a fresh session without going
through NewUserAuthTokenResponse, so those logins were missing from the
audit trail. The refresh grant stays unaudited like the v1 refresh.
This commit is contained in:
kolaente 2026-06-12 10:43:38 +02:00
parent a56ae39b36
commit 7da3607032
2 changed files with 15 additions and 5 deletions

View File

@ -17,10 +17,14 @@
package oauth2server
import (
"context"
"net/http"
"code.vikunja.io/api/pkg/config"
"code.vikunja.io/api/pkg/db"
"code.vikunja.io/api/pkg/events"
"code.vikunja.io/api/pkg/log"
"code.vikunja.io/api/pkg/models"
"code.vikunja.io/api/pkg/modules/auth"
"code.vikunja.io/api/pkg/user"
@ -56,7 +60,7 @@ func HandleToken(c *echo.Context) error {
return echo.NewHTTPError(http.StatusBadRequest, "Invalid request body")
}
resp, err := ExchangeToken(&req, c.Request().UserAgent(), c.RealIP())
resp, err := ExchangeToken(c.Request().Context(), &req, c.Request().UserAgent(), c.RealIP())
if err != nil {
return err
}
@ -69,10 +73,10 @@ func HandleToken(c *echo.Context) error {
// token endpoint, independent of the HTTP layer. Callers own request binding and
// the Cache-Control: no-store response header. deviceInfo/ipAddress are recorded
// on the session created for the authorization_code grant.
func ExchangeToken(req *TokenRequest, deviceInfo, ipAddress string) (*TokenResponse, error) {
func ExchangeToken(ctx context.Context, req *TokenRequest, deviceInfo, ipAddress string) (*TokenResponse, error) {
switch req.GrantType {
case "authorization_code":
return exchangeAuthorizationCode(req, deviceInfo, ipAddress)
return exchangeAuthorizationCode(ctx, req, deviceInfo, ipAddress)
case "refresh_token":
return exchangeRefreshToken(req)
default:
@ -80,7 +84,7 @@ func ExchangeToken(req *TokenRequest, deviceInfo, ipAddress string) (*TokenRespo
}
}
func exchangeAuthorizationCode(req *TokenRequest, deviceInfo, ipAddress string) (*TokenResponse, error) {
func exchangeAuthorizationCode(ctx context.Context, req *TokenRequest, deviceInfo, ipAddress string) (*TokenResponse, error) {
s := db.NewSession()
defer s.Close()
@ -133,6 +137,12 @@ func exchangeAuthorizationCode(req *TokenRequest, deviceInfo, ipAddress string)
return nil, err
}
// The code exchange mints a fresh session, so it is a login for the
// audit trail, same as NewUserAuthTokenResponse.
if err := events.DispatchWithContext(ctx, &user.LoginSucceededEvent{User: u}); err != nil {
log.Errorf("Could not dispatch login succeeded event: %s", err)
}
return &TokenResponse{
AccessToken: accessToken,
TokenType: "bearer",

View File

@ -76,7 +76,7 @@ func oauthToken(ctx context.Context, in *struct {
Body oauth2server.TokenRequest `contentType:"application/x-www-form-urlencoded"`
}) (*oauthTokenBody, error) {
deviceInfo, ipAddress := requestClientInfo(ctx)
resp, err := oauth2server.ExchangeToken(&in.Body, deviceInfo, ipAddress)
resp, err := oauth2server.ExchangeToken(ctx, &in.Body, deviceInfo, ipAddress)
if err != nil {
return nil, translateDomainError(err)
}