using System.Security.Cryptography.X509Certificates; using System.Net.Security; using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using IdentityModel.Client; using Xunit; using Yavsc.Server.Helpers; namespace Yavsc.Org.Tests { [Collection("Yavsc Server")] [Trait("regression", "oui")] public class Remoting : BaseTestContext, IClassFixture { private readonly ITestOutputHelper _output; public Remoting(WebServerFixture serverFixture, ITestOutputHelper output) : base(output, serverFixture) { _output = output; } [Fact] public async Task ObtainServiceToken() { var serverUrl = GetServerUrl(); var cancellationToken = TestContext.Current.CancellationToken; HttpClient client = NewHttpClient(); var tokenEndpoint = await ResolveTokenEndpointAsync(client, serverUrl, cancellationToken); var response = await client.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest { Address = tokenEndpoint, ClientId = RequireNonEmpty(_serverFixture.TestClientId, nameof(_serverFixture.TestClientId)), ClientSecret = RequireNonEmpty(_serverFixture.TestClientSecret, nameof(_serverFixture.TestClientSecret)), Scope = "test", GrantType = "client_credentials" }, cancellationToken); if (response.IsError) throw new Exception(response.Error); } private static HttpClient NewHttpClient() { return new HttpClient(new BypassSslValidationHandler()); } [Fact] public async Task ObtainResourceOwnerPasswordToken() { var serverUrl = GetServerUrl(); var cancellationToken = TestContext.Current.CancellationToken; var client = NewHttpClient(); var tokenEndpoint = await ResolveTokenEndpointAsync(client, serverUrl, cancellationToken); var response = await client.RequestPasswordTokenAsync(new PasswordTokenRequest { Address = tokenEndpoint, ClientId = RequireNonEmpty(_serverFixture.TestClientId, nameof(_serverFixture.TestClientId)), ClientSecret = RequireNonEmpty(_serverFixture.TestClientSecret, nameof(_serverFixture.TestClientSecret)), UserName = RequireNonEmpty(_serverFixture.TestingUserName, nameof(_serverFixture.TestingUserName)), Password = RequireNonEmpty(_serverFixture.TestingUserPassword, nameof(_serverFixture.TestingUserPassword)), Scope = "test", Parameters = { { "acr_values", "tenant:custom_account_store1 foo bar quux" } } }, cancellationToken); if (response.IsError) throw new Exception(response.Error); } /// /// Regression for the MyFiles upload 500 / null /// User.Identity.Name: the access token minted by the /// real IdentityServer (resource-owner password grant) must /// carry a name claim whose value is the user's login /// (UserName), and /// — which reads FindFirstValue("name") — must resolve /// to it on a principal built the way /// AddYavscJwtBearer builds one /// (MapInboundClaims=false, NameClaimType="name"). /// /// Prerequisites wired by : /// the test client's AllowedScopes include /// openid+profile, and the standard identity /// resources are seeded by the production pipeline. Requesting /// openid profile makes ProfileService emit /// name = user.UserName. /// [Fact] public async Task PasswordToken_carries_name_claim_equal_to_login() { var serverUrl = GetServerUrl(); var cancellationToken = TestContext.Current.CancellationToken; var client = NewHttpClient(); var tokenEndpoint = await ResolveTokenEndpointAsync(client, serverUrl, cancellationToken); var response = await client.RequestPasswordTokenAsync(new PasswordTokenRequest { Address = tokenEndpoint, ClientId = RequireNonEmpty(_serverFixture.TestClientId, nameof(_serverFixture.TestClientId)), ClientSecret = RequireNonEmpty(_serverFixture.TestClientSecret, nameof(_serverFixture.TestClientSecret)), UserName = RequireNonEmpty(_serverFixture.TestingUserName, nameof(_serverFixture.TestingUserName)), Password = RequireNonEmpty(_serverFixture.TestingUserPassword, nameof(_serverFixture.TestingUserPassword)), // "openid profile" triggers ProfileService to emit the // "name" claim (user.UserName). Without profile, the // token carries only sub + scopes and GetUserName() // resolves to null — the exact regression behind the // MyFiles upload 500. "test" is the API scope, so a real // JWT access token (not just an id_token) is issued. Scope = "openid profile test" }, cancellationToken); Assert.False(response.IsError, $"Token request failed: {response.Error} ({response.ErrorDescription})"); // Decode the access token straight from the payload so short // claim names ("name", "sub") stay as-emitted — mirrors // AddYavscJwtBearer with MapInboundClaims=false. var jwt = new JwtSecurityToken(response.AccessToken); var nameClaim = jwt.Claims.FirstOrDefault(c => c.Type == "name"); Assert.True(nameClaim is not null, "access_token has no 'name' claim; ProfileService did not emit it " + "(is the 'profile' scope in the client's AllowedScopes and requested?)."); Assert.Equal(_serverFixture.TestingUserName, nameClaim!.Value); // Rebuild the principal the way a bearer consumer host does // and exercise the real GetUserName() helper used by the // file-system / avatar paths. var identity = new ClaimsIdentity( jwt.Claims, authenticationType: "Bearer", nameType: "name", roleType: null); var principal = new ClaimsPrincipal(identity); Assert.Equal(_serverFixture.TestingUserName, principal.GetUserName()); } [Fact] public async Task GetSignin_returns_a_page() { using var client = new HttpClient(new BypassSslValidationHandler()) { BaseAddress = new Uri(this._serverFixture.HttpsAuthority ?? throw new InvalidOperationException("Missing HttpsAuthority")) }; await AssertResponds(client, "/signin"); } [Fact] public async Task GetOpenIdConfiguration_returns_ok() { using var client = CreateHttpClient(); var response = await GetRaw(client, "/.well-known/openid-configuration"); var payload = await response.Content.ReadAsStringAsync( TestContext.Current.CancellationToken ); Assert.True( response.IsSuccessStatusCode, $"GET /.well-known/openid-configuration returned {(int)response.StatusCode} {response.StatusCode}. Body: {payload}"); } public static IEnumerable GetLoginIntentData() { return new object[][] { new object[] { "testuser", "test" } }; } private async Task ResolveTokenEndpointAsync(HttpClient client, string serverUrl, CancellationToken cancellationToken) { var disco = await client.GetDiscoveryDocumentAsync(serverUrl, cancellationToken); if (!disco.IsError && !string.IsNullOrWhiteSpace(disco.TokenEndpoint)) { return disco.TokenEndpoint; } // Some full-suite runs intermittently return 500 on the OIDC // discovery document while /connect/token remains available. var fallback = new Uri(new Uri(serverUrl), "/connect/token").ToString(); _output.WriteLine($"WARNING: OIDC discovery failed ({disco.Error}). Fallback token endpoint: {fallback}"); return fallback; } private string GetServerUrl() { return RequireNonEmpty(_serverFixture.SiteSettings?.Authority, "SiteSettings.Authority"); } private static string RequireNonEmpty(string? value, string name) { if (string.IsNullOrWhiteSpace(value)) { throw new InvalidOperationException($"Missing required test setting: {name}"); } return value; } } internal class BypassSslValidationHandler : HttpClientHandler { public BypassSslValidationHandler() { // Override validation for this handler only ServerCertificateCustomValidationCallback = ValidateCertificate; } private bool ValidateCertificate( HttpRequestMessage request, X509Certificate2? certificate, X509Chain? chain, SslPolicyErrors errors) { // Accept all certificates (bypass validation) return true; } } }