|
| 1 | +using System.Text; |
| 2 | +using System.Text.Json; |
1 | 3 | using GithubActionsOrchestrator.Auth; |
2 | 4 | using GithubActionsOrchestrator.Database; |
3 | 5 | using GithubActionsOrchestrator.Models; |
@@ -678,6 +680,82 @@ public IResult Health() |
678 | 680 | return Results.Ok(new { status = "healthy" }); |
679 | 681 | } |
680 | 682 |
|
| 683 | + // Debug helper for tuning Teleport auth. Decodes the incoming JWT WITHOUT verifying |
| 684 | + // its signature so you can read the real iss/aud/roles/sub claim names/values even |
| 685 | + // when validation is currently failing, and compares them to what's configured. |
| 686 | + // Only ever exposes the caller's OWN token. API-key gated like the rest of /api. |
| 687 | + [Route("auth-debug")] |
| 688 | + [HttpGet] |
| 689 | + public IResult AuthDebug() |
| 690 | + { |
| 691 | + var cfg = Program.Config.TeleportAuth; |
| 692 | + string raw = Request.Headers[TeleportAuth.JwtHeader].FirstOrDefault(); |
| 693 | + |
| 694 | + object tokenHeader = null; |
| 695 | + object tokenClaims = null; |
| 696 | + string decodeError = null; |
| 697 | + if (!string.IsNullOrEmpty(raw)) |
| 698 | + { |
| 699 | + try |
| 700 | + { |
| 701 | + var parts = raw.Split('.'); |
| 702 | + if (parts.Length >= 2) |
| 703 | + { |
| 704 | + tokenHeader = JsonSerializer.Deserialize<JsonElement>(DecodeJwtSegment(parts[0])); |
| 705 | + tokenClaims = JsonSerializer.Deserialize<JsonElement>(DecodeJwtSegment(parts[1])); |
| 706 | + } |
| 707 | + else |
| 708 | + { |
| 709 | + decodeError = "Header present but not a well-formed JWT (expected 3 dot-separated parts)."; |
| 710 | + } |
| 711 | + } |
| 712 | + catch (Exception ex) |
| 713 | + { |
| 714 | + decodeError = ex.Message; |
| 715 | + } |
| 716 | + } |
| 717 | + |
| 718 | + return Results.Json(new |
| 719 | + { |
| 720 | + // What the orchestrator is currently configured to expect. |
| 721 | + expected = new |
| 722 | + { |
| 723 | + enabled = cfg.Enabled, |
| 724 | + jwksUrl = cfg.JwksUrl, |
| 725 | + issuer = cfg.Issuer, |
| 726 | + audience = cfg.Audience, |
| 727 | + authorizedRoles = cfg.AuthorizedRoles, |
| 728 | + authorizedUsers = cfg.AuthorizedUsers, |
| 729 | + }, |
| 730 | + // Whether Teleport actually forwarded a token to us. |
| 731 | + headerName = TeleportAuth.JwtHeader, |
| 732 | + headerPresent = !string.IsNullOrEmpty(raw), |
| 733 | + // Decoded but UNVERIFIED — read iss/aud/roles/sub/kid here to fill in config. |
| 734 | + tokenHeader, |
| 735 | + tokenClaims, |
| 736 | + decodeError, |
| 737 | + // Result of real verification (empty/false when validation fails). |
| 738 | + validated = new |
| 739 | + { |
| 740 | + authenticated = User?.Identity?.IsAuthenticated == true, |
| 741 | + user = TeleportAuth.GetUsername(User), |
| 742 | + roles = TeleportAuth.GetRoles(User).Distinct().ToArray(), |
| 743 | + canMutate = TeleportAuth.IsAuthorizedToMutate(User, cfg), |
| 744 | + }, |
| 745 | + }); |
| 746 | + } |
| 747 | + |
| 748 | + private static string DecodeJwtSegment(string segment) |
| 749 | + { |
| 750 | + string s = segment.Replace('-', '+').Replace('_', '/'); |
| 751 | + switch (s.Length % 4) |
| 752 | + { |
| 753 | + case 2: s += "=="; break; |
| 754 | + case 3: s += "="; break; |
| 755 | + } |
| 756 | + return Encoding.UTF8.GetString(Convert.FromBase64String(s)); |
| 757 | + } |
| 758 | + |
681 | 759 | // Identity of the caller (from the verified Teleport JWT, if present) plus whether |
682 | 760 | // they may run mutating actions. The frontend uses this to gate its action buttons. |
683 | 761 | [Route("me")] |
|
0 commit comments