forked from baron/baron-sso
chore(headless-login): add request correlation logs
This commit is contained in:
@@ -126,6 +126,34 @@ type headlessClientAssertionClaims struct {
|
||||
|
||||
type headlessAssertionAud []string
|
||||
|
||||
type headlessLoginFailure struct {
|
||||
status int
|
||||
code string
|
||||
safeMessage string
|
||||
logMessage string
|
||||
debugFields map[string]any
|
||||
}
|
||||
|
||||
func (e *headlessLoginFailure) Error() string {
|
||||
if e == nil {
|
||||
return ""
|
||||
}
|
||||
if e.code != "" {
|
||||
return e.code
|
||||
}
|
||||
return e.safeMessage
|
||||
}
|
||||
|
||||
func newHeadlessLoginFailure(status int, code, safeMessage, logMessage string, debugFields map[string]any) *headlessLoginFailure {
|
||||
return &headlessLoginFailure{
|
||||
status: status,
|
||||
code: code,
|
||||
safeMessage: safeMessage,
|
||||
logMessage: logMessage,
|
||||
debugFields: debugFields,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *headlessAssertionAud) UnmarshalJSON(data []byte) error {
|
||||
var single string
|
||||
if err := json.Unmarshal(data, &single); err == nil {
|
||||
@@ -1742,6 +1770,117 @@ func containsHeadlessAudience(expected []string, actual headlessAssertionAud) bo
|
||||
return false
|
||||
}
|
||||
|
||||
func headlessRequestID(c *fiber.Ctx) string {
|
||||
if c == nil {
|
||||
return ""
|
||||
}
|
||||
reqID := strings.TrimSpace(c.GetRespHeader(fiber.HeaderXRequestID))
|
||||
if reqID != "" {
|
||||
return reqID
|
||||
}
|
||||
return strings.TrimSpace(c.Get(fiber.HeaderXRequestID))
|
||||
}
|
||||
|
||||
func isHeadlessDebugLoggingEnabled() bool {
|
||||
return slog.Default().Enabled(context.Background(), slog.LevelDebug)
|
||||
}
|
||||
|
||||
func truncateHeadlessLogValue(value string, limit int) string {
|
||||
value = strings.TrimSpace(value)
|
||||
if limit <= 0 || len(value) <= limit {
|
||||
return value
|
||||
}
|
||||
return value[:limit]
|
||||
}
|
||||
|
||||
func logHeadlessLoginFailure(c *fiber.Ctx, message string, failure *headlessLoginFailure, clientID, loginChallenge string) {
|
||||
if failure == nil {
|
||||
return
|
||||
}
|
||||
|
||||
args := []any{
|
||||
"reason_code", failure.code,
|
||||
"client_id", strings.TrimSpace(clientID),
|
||||
"path", c.Path(),
|
||||
}
|
||||
|
||||
if reqID := headlessRequestID(c); reqID != "" {
|
||||
args = append(args, "req_id", reqID)
|
||||
}
|
||||
|
||||
if trimmedChallenge := truncateHeadlessLogValue(loginChallenge, 12); trimmedChallenge != "" {
|
||||
args = append(args, "login_challenge_prefix", trimmedChallenge)
|
||||
}
|
||||
|
||||
if isHeadlessDebugLoggingEnabled() {
|
||||
keys := make([]string, 0, len(failure.debugFields))
|
||||
for key := range failure.debugFields {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for _, key := range keys {
|
||||
args = append(args, key, failure.debugFields[key])
|
||||
}
|
||||
}
|
||||
|
||||
level := slog.LevelWarn
|
||||
if failure.status >= 500 {
|
||||
level = slog.LevelError
|
||||
}
|
||||
slog.Log(context.Background(), level, message, args...)
|
||||
}
|
||||
|
||||
func logHeadlessLoginSuccess(c *fiber.Ctx, clientID, loginChallenge, redirectTo string) {
|
||||
args := []any{
|
||||
"client_id", strings.TrimSpace(clientID),
|
||||
"path", c.Path(),
|
||||
"response_status", fiber.StatusOK,
|
||||
}
|
||||
|
||||
if reqID := headlessRequestID(c); reqID != "" {
|
||||
args = append(args, "req_id", reqID)
|
||||
}
|
||||
|
||||
if trimmedChallenge := truncateHeadlessLogValue(loginChallenge, 12); trimmedChallenge != "" {
|
||||
args = append(args, "login_challenge_prefix", trimmedChallenge)
|
||||
}
|
||||
|
||||
parsed, err := url.Parse(redirectTo)
|
||||
if err != nil {
|
||||
args = append(args, "redirect_to_length", len(redirectTo), "redirect_parse_error", err.Error())
|
||||
slog.Info("headless password login succeeded", args...)
|
||||
return
|
||||
}
|
||||
|
||||
query := parsed.Query()
|
||||
args = append(
|
||||
args,
|
||||
"redirect_to_length", len(redirectTo),
|
||||
"redirect_to_host", parsed.Host,
|
||||
"redirect_to_path", parsed.Path,
|
||||
"redirect_has_login_verifier", query.Has("login_verifier"),
|
||||
"redirect_has_redirect_uri", query.Has("redirect_uri"),
|
||||
)
|
||||
slog.Info("headless password login succeeded", args...)
|
||||
}
|
||||
|
||||
func respondHeadlessLoginFailure(c *fiber.Ctx, failure *headlessLoginFailure) error {
|
||||
if failure == nil {
|
||||
return nil
|
||||
}
|
||||
return errorJSONCode(c, failure.status, failure.code, failure.safeMessage)
|
||||
}
|
||||
|
||||
func newHeadlessCredentialFailure(status int, code, safeMessage string) *headlessLoginFailure {
|
||||
return newHeadlessLoginFailure(
|
||||
status,
|
||||
code,
|
||||
safeMessage,
|
||||
"headless password login credential authentication failed",
|
||||
nil,
|
||||
)
|
||||
}
|
||||
|
||||
func (h *AuthHandler) loadHeadlessJWKS(ctx context.Context, client domain.HydraClient, expectedKid string) (*jose.JSONWebKeySet, bool, error) {
|
||||
if h.HeadlessJWKS == nil {
|
||||
h.HeadlessJWKS = service.NewHeadlessJWKSCacheService(h.RedisService, nil)
|
||||
@@ -1753,30 +1892,75 @@ func (h *AuthHandler) loadHeadlessJWKS(ctx context.Context, client domain.HydraC
|
||||
return keySet, refreshed, nil
|
||||
}
|
||||
|
||||
func validateHeadlessClientAssertionClaims(c *fiber.Ctx, claims headlessClientAssertionClaims, clientID string) error {
|
||||
func validateHeadlessClientAssertionClaims(c *fiber.Ctx, claims headlessClientAssertionClaims, clientID string) *headlessLoginFailure {
|
||||
now := time.Now().Unix()
|
||||
debugFields := map[string]any{
|
||||
"claim_issuer": claims.Issuer,
|
||||
"claim_subject": claims.Subject,
|
||||
"claim_expires_at": claims.ExpiresAt,
|
||||
"claim_not_before": claims.NotBefore,
|
||||
"claim_issued_at": claims.IssuedAt,
|
||||
"received_audiences": []string(claims.Audience),
|
||||
"expected_audiences": headlessAssertionAudiences(c),
|
||||
}
|
||||
if claims.Issuer != clientID || claims.Subject != clientID {
|
||||
return fmt.Errorf("client assertion iss/sub mismatch")
|
||||
return newHeadlessLoginFailure(
|
||||
fiber.StatusUnauthorized,
|
||||
"invalid_client_assertion_iss_sub",
|
||||
"Client assertion issuer or subject mismatch",
|
||||
"headless password login client assertion claims mismatch",
|
||||
debugFields,
|
||||
)
|
||||
}
|
||||
if claims.ExpiresAt == 0 || claims.ExpiresAt <= now {
|
||||
return fmt.Errorf("client assertion expired")
|
||||
return newHeadlessLoginFailure(
|
||||
fiber.StatusUnauthorized,
|
||||
"invalid_client_assertion_expired",
|
||||
"Client assertion has expired",
|
||||
"headless password login client assertion expired",
|
||||
debugFields,
|
||||
)
|
||||
}
|
||||
if claims.NotBefore != 0 && claims.NotBefore > now {
|
||||
return fmt.Errorf("client assertion not active yet")
|
||||
return newHeadlessLoginFailure(
|
||||
fiber.StatusUnauthorized,
|
||||
"invalid_client_assertion_not_before",
|
||||
"Client assertion is not active yet",
|
||||
"headless password login client assertion not active yet",
|
||||
debugFields,
|
||||
)
|
||||
}
|
||||
if claims.IssuedAt != 0 && claims.IssuedAt > now+60 {
|
||||
return fmt.Errorf("client assertion issued in the future")
|
||||
return newHeadlessLoginFailure(
|
||||
fiber.StatusUnauthorized,
|
||||
"invalid_client_assertion_iat_future",
|
||||
"Client assertion issued-at time is invalid",
|
||||
"headless password login client assertion issued in the future",
|
||||
debugFields,
|
||||
)
|
||||
}
|
||||
if !containsHeadlessAudience(headlessAssertionAudiences(c), claims.Audience) {
|
||||
return fmt.Errorf("client assertion audience mismatch")
|
||||
return newHeadlessLoginFailure(
|
||||
fiber.StatusUnauthorized,
|
||||
"invalid_client_assertion_audience",
|
||||
"Client assertion audience mismatch",
|
||||
"headless password login client assertion audience mismatch",
|
||||
debugFields,
|
||||
)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *AuthHandler) verifyHeadlessClientAssertion(c *fiber.Ctx, client domain.HydraClient, clientID, clientAssertion string) error {
|
||||
func (h *AuthHandler) verifyHeadlessClientAssertion(c *fiber.Ctx, client domain.HydraClient, clientID, clientAssertion string) *headlessLoginFailure {
|
||||
assertion := strings.TrimSpace(clientAssertion)
|
||||
if assertion == "" {
|
||||
return errorJSONCode(c, fiber.StatusBadRequest, "bad_request", "client_assertion is required")
|
||||
return newHeadlessLoginFailure(
|
||||
fiber.StatusBadRequest,
|
||||
"bad_request",
|
||||
"client_assertion is required",
|
||||
"headless password login client assertion missing",
|
||||
nil,
|
||||
)
|
||||
}
|
||||
|
||||
token, err := josejwt.ParseSigned(assertion, []jose.SignatureAlgorithm{
|
||||
@@ -1786,7 +1970,13 @@ func (h *AuthHandler) verifyHeadlessClientAssertion(c *fiber.Ctx, client domain.
|
||||
jose.EdDSA,
|
||||
})
|
||||
if err != nil {
|
||||
return errorJSONCode(c, fiber.StatusUnauthorized, "invalid_client_assertion", "Failed to verify client assertion")
|
||||
return newHeadlessLoginFailure(
|
||||
fiber.StatusUnauthorized,
|
||||
"invalid_client_assertion_parse",
|
||||
"Client assertion format is invalid",
|
||||
"headless password login client assertion parse failed",
|
||||
nil,
|
||||
)
|
||||
}
|
||||
|
||||
expectedKid := ""
|
||||
@@ -1797,7 +1987,15 @@ func (h *AuthHandler) verifyHeadlessClientAssertion(c *fiber.Ctx, client domain.
|
||||
keySet, refreshed, err := h.loadHeadlessJWKS(c.Context(), client, expectedKid)
|
||||
if err != nil {
|
||||
slog.Error("failed to load jwks for headless client assertion", "clientID", clientID, "error", err)
|
||||
return errorJSONCode(c, fiber.StatusUnauthorized, "invalid_client_assertion", headlessClientAssertionErrorMessage(err))
|
||||
return newHeadlessLoginFailure(
|
||||
fiber.StatusUnauthorized,
|
||||
"invalid_client_assertion_jwks_load",
|
||||
headlessClientAssertionErrorMessage(err),
|
||||
"headless password login client assertion jwks load failed",
|
||||
map[string]any{
|
||||
"received_kid": expectedKid,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
matchingKidPresent := expectedKid != "" && containsHeadlessKeyID(keySet, expectedKid)
|
||||
@@ -1810,8 +2008,13 @@ func (h *AuthHandler) verifyHeadlessClientAssertion(c *fiber.Ctx, client domain.
|
||||
if err := token.Claims(key.Key, &claims); err != nil {
|
||||
continue
|
||||
}
|
||||
if err := validateHeadlessClientAssertionClaims(c, claims, clientID); err != nil {
|
||||
return errorJSONCode(c, fiber.StatusUnauthorized, "invalid_client_assertion", "Failed to verify client assertion claims")
|
||||
if failure := validateHeadlessClientAssertionClaims(c, claims, clientID); failure != nil {
|
||||
if failure.debugFields == nil {
|
||||
failure.debugFields = map[string]any{}
|
||||
}
|
||||
failure.debugFields["received_kid"] = expectedKid
|
||||
failure.debugFields["jwks_refreshed"] = refreshed
|
||||
return failure
|
||||
}
|
||||
_ = h.HeadlessJWKS.MarkVerificationSuccess(clientID)
|
||||
return nil
|
||||
@@ -1829,8 +2032,13 @@ func (h *AuthHandler) verifyHeadlessClientAssertion(c *fiber.Ctx, client domain.
|
||||
if err := token.Claims(key.Key, &claims); err != nil {
|
||||
continue
|
||||
}
|
||||
if err := validateHeadlessClientAssertionClaims(c, claims, clientID); err != nil {
|
||||
return errorJSONCode(c, fiber.StatusUnauthorized, "invalid_client_assertion", "Failed to verify client assertion claims")
|
||||
if failure := validateHeadlessClientAssertionClaims(c, claims, clientID); failure != nil {
|
||||
if failure.debugFields == nil {
|
||||
failure.debugFields = map[string]any{}
|
||||
}
|
||||
failure.debugFields["received_kid"] = expectedKid
|
||||
failure.debugFields["jwks_refreshed"] = true
|
||||
return failure
|
||||
}
|
||||
_ = h.HeadlessJWKS.MarkVerificationSuccess(clientID)
|
||||
return nil
|
||||
@@ -1838,7 +2046,16 @@ func (h *AuthHandler) verifyHeadlessClientAssertion(c *fiber.Ctx, client domain.
|
||||
}
|
||||
}
|
||||
|
||||
return errorJSONCode(c, fiber.StatusUnauthorized, "invalid_client_assertion", "Failed to verify client assertion signature with jwksUri")
|
||||
return newHeadlessLoginFailure(
|
||||
fiber.StatusUnauthorized,
|
||||
"invalid_client_assertion_signature",
|
||||
"Client assertion signature verification failed",
|
||||
"headless password login client assertion signature verification failed",
|
||||
map[string]any{
|
||||
"received_kid": expectedKid,
|
||||
"jwks_refreshed": refreshed,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func headlessClientAssertionErrorMessage(err error) string {
|
||||
@@ -2045,13 +2262,21 @@ func (h *AuthHandler) HeadlessPasswordLogin(c *fiber.Ctx) error {
|
||||
if err := h.validateHeadlessPasswordLoginClient(loginReq, clientID); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := h.verifyHeadlessClientAssertion(c, loginReq.Client, clientID, req.ClientAssertion); err != nil {
|
||||
return err
|
||||
if failure := h.verifyHeadlessClientAssertion(c, loginReq.Client, clientID, req.ClientAssertion); failure != nil {
|
||||
logHeadlessLoginFailure(c, failure.logMessage, failure, clientID, loginChallenge)
|
||||
return respondHeadlessLoginFailure(c, failure)
|
||||
}
|
||||
|
||||
authInfo, authErr := h.authenticatePasswordLogin(c.Context(), loginID, req.Password)
|
||||
if authErr != nil {
|
||||
status, code, message := passwordLoginErrorSpec(authErr)
|
||||
logHeadlessLoginFailure(
|
||||
c,
|
||||
"headless password login credential authentication failed",
|
||||
newHeadlessCredentialFailure(status, code, message),
|
||||
clientID,
|
||||
loginChallenge,
|
||||
)
|
||||
return errorJSONCode(c, status, code, message)
|
||||
}
|
||||
|
||||
@@ -2064,11 +2289,15 @@ func (h *AuthHandler) HeadlessPasswordLogin(c *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
logOidcRedirectSummary("headless_password_login", acceptResp.RedirectTo)
|
||||
return c.JSON(fiber.Map{
|
||||
if err := c.Status(fiber.StatusOK).JSON(fiber.Map{
|
||||
"redirectTo": acceptResp.RedirectTo,
|
||||
"status": "ok",
|
||||
"provider": h.IdpProvider.Name(),
|
||||
})
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
logHeadlessLoginSuccess(c, clientID, loginChallenge, acceptResp.RedirectTo)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *AuthHandler) startHeadlessPhoneLink(c *fiber.Ctx, loginID string) (fiber.Map, string, string, time.Duration, error) {
|
||||
@@ -2194,8 +2423,9 @@ func (h *AuthHandler) HeadlessLinkInit(c *fiber.Ctx) error {
|
||||
if err := h.validateHeadlessPasswordLoginClient(loginReq, clientID); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := h.verifyHeadlessClientAssertion(c, loginReq.Client, clientID, req.ClientAssertion); err != nil {
|
||||
return err
|
||||
if failure := h.verifyHeadlessClientAssertion(c, loginReq.Client, clientID, req.ClientAssertion); failure != nil {
|
||||
logHeadlessLoginFailure(c, failure.logMessage, failure, clientID, loginChallenge)
|
||||
return respondHeadlessLoginFailure(c, failure)
|
||||
}
|
||||
|
||||
resp, pendingRef, resolvedLoginID, ttl, err := h.startHeadlessPhoneLink(c, loginID)
|
||||
@@ -2255,8 +2485,9 @@ func (h *AuthHandler) HeadlessLinkPoll(c *fiber.Ctx) error {
|
||||
if err := h.validateHeadlessPasswordLoginClient(loginReq, clientID); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := h.verifyHeadlessClientAssertion(c, loginReq.Client, clientID, req.ClientAssertion); err != nil {
|
||||
return err
|
||||
if failure := h.verifyHeadlessClientAssertion(c, loginReq.Client, clientID, req.ClientAssertion); failure != nil {
|
||||
logHeadlessLoginFailure(c, failure.logMessage, failure, clientID, state.LoginChallenge)
|
||||
return respondHeadlessLoginFailure(c, failure)
|
||||
}
|
||||
|
||||
val, err := h.RedisService.Get(prefixSession + pendingRef)
|
||||
|
||||
Reference in New Issue
Block a user