|
| 1 | +using System.IdentityModel.Tokens.Jwt; |
1 | 2 | using System.Security.Claims; |
| 3 | +using System.Text; |
2 | 4 | using System.Text.Encodings.Web; |
| 5 | +using CCE.Application.Identity.Auth.Common; |
3 | 6 | using Microsoft.AspNetCore.Authentication; |
4 | 7 | using Microsoft.Extensions.Logging; |
5 | 8 | using Microsoft.Extensions.Options; |
| 9 | +using Microsoft.IdentityModel.Tokens; |
6 | 10 |
|
7 | 11 | namespace CCE.Api.Common.Auth; |
8 | 12 |
|
@@ -51,11 +55,17 @@ public sealed class DevAuthHandler : AuthenticationHandler<AuthenticationSchemeO |
51 | 55 | ["cce-user"] = Guid.Parse("aaaaaaaa-aaaa-aaaa-aaaa-000000000005"), |
52 | 56 | }; |
53 | 57 |
|
| 58 | + private readonly IOptions<LocalAuthOptions> _localAuthOptions; |
| 59 | + |
54 | 60 | public DevAuthHandler( |
55 | 61 | IOptionsMonitor<AuthenticationSchemeOptions> options, |
56 | 62 | ILoggerFactory logger, |
57 | | - UrlEncoder encoder) |
58 | | - : base(options, logger, encoder) { } |
| 63 | + UrlEncoder encoder, |
| 64 | + IOptions<LocalAuthOptions> localAuthOptions) |
| 65 | + : base(options, logger, encoder) |
| 66 | + { |
| 67 | + _localAuthOptions = localAuthOptions; |
| 68 | + } |
59 | 69 |
|
60 | 70 | protected override Task<AuthenticateResult> HandleAuthenticateAsync() |
61 | 71 | { |
@@ -96,11 +106,60 @@ protected override Task<AuthenticateResult> HandleAuthenticateAsync() |
96 | 106 | if (Request.Headers.TryGetValue("Authorization", out var auth)) |
97 | 107 | { |
98 | 108 | var raw = auth.ToString(); |
| 109 | + |
99 | 110 | const string devPrefix = "Bearer dev:"; |
100 | 111 | if (raw.StartsWith(devPrefix, StringComparison.OrdinalIgnoreCase)) |
101 | 112 | { |
102 | 113 | return raw.Substring(devPrefix.Length).Trim(); |
103 | 114 | } |
| 115 | + |
| 116 | + // Fallback: try to decode as a real JWT (e.g. issued by /api/auth/login) |
| 117 | + const string bearerPrefix = "Bearer "; |
| 118 | + if (raw.StartsWith(bearerPrefix, StringComparison.OrdinalIgnoreCase)) |
| 119 | + { |
| 120 | + var token = raw.Substring(bearerPrefix.Length).Trim(); |
| 121 | + return TryReadRoleFromJwt(token); |
| 122 | + } |
| 123 | + } |
| 124 | + |
| 125 | + return null; |
| 126 | + } |
| 127 | + |
| 128 | + private string? TryReadRoleFromJwt(string token) |
| 129 | + { |
| 130 | + try |
| 131 | + { |
| 132 | + var opts = _localAuthOptions.Value; |
| 133 | + var profiles = new[] { opts.External, opts.Internal }; |
| 134 | + var handler = new JwtSecurityTokenHandler { MapInboundClaims = false }; |
| 135 | + |
| 136 | + foreach (var profile in profiles) |
| 137 | + { |
| 138 | + if (string.IsNullOrWhiteSpace(profile.SigningKey)) |
| 139 | + continue; |
| 140 | + |
| 141 | + var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(profile.SigningKey)); |
| 142 | + var parameters = new TokenValidationParameters |
| 143 | + { |
| 144 | + ValidateIssuer = true, |
| 145 | + ValidIssuer = profile.Issuer, |
| 146 | + ValidateAudience = true, |
| 147 | + ValidAudience = profile.Audience, |
| 148 | + ValidateIssuerSigningKey = true, |
| 149 | + IssuerSigningKey = key, |
| 150 | + ValidateLifetime = true, |
| 151 | + ClockSkew = TimeSpan.FromMinutes(2), |
| 152 | + }; |
| 153 | + |
| 154 | + var principal = handler.ValidateToken(token, parameters, out _); |
| 155 | + var role = principal.FindFirst("roles")?.Value; |
| 156 | + if (!string.IsNullOrEmpty(role)) |
| 157 | + return role; |
| 158 | + } |
| 159 | + } |
| 160 | + catch (Exception ex) |
| 161 | + { |
| 162 | + Logger.LogDebug(ex, "Failed to validate JWT in DevAuthHandler fallback"); |
104 | 163 | } |
105 | 164 |
|
106 | 165 | return null; |
|
0 commit comments