using System.Net; using System.Net.Http.Headers; using System.Net.Http.Json; using System.Text.Json.Serialization; using IdentityShroud.Api.Apis; using IdentityShroud.Core.EFCore; using IdentityShroud.Core.Tests.Fixtures; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Shouldly; namespace IdentityShroud.Api.Tests.Apis; public class OpenIdApiTests : IClassFixture { private readonly ApplicationFactory _factory; public OpenIdApiTests(ApplicationFactory factory) { _factory = factory; using var scope = _factory.Services.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); if (!db.Database.EnsureCreated()) { db.Database.ExecuteSqlRaw("TRUNCATE realm CASCADE;"); } } [Theory] [InlineData(true)] [InlineData(false)] public async Task ClientCredentialsFlow(bool useAuthenticationHeader) { var client = _factory.CreateClient(); var createRealmResponse = await client.PostAsync("/api/v1/realms", JsonContent.Create(new { Slug = "foo", Name = "Test'", }), TestContext.Current.CancellationToken); createRealmResponse.StatusCode.ShouldBe(HttpStatusCode.Created); var realm = await createRealmResponse.Content.ReadFromJsonAsync( cancellationToken: TestContext.Current.CancellationToken); realm.ShouldNotBeNull(); realm.Id.ShouldNotBe(Guid.Empty); var createClientResponse = await client.PostAsync( $"/api/v1/realms/{realm.Id}/clients", JsonContent.Create(new { ClientId = "myclient", Name = "New Client", Confidential = true, AllowClientCredentialsFlow = true, GenerateSecret = true, }), TestContext.Current.CancellationToken); createClientResponse.StatusCode.ShouldBe(HttpStatusCode.Created); // Act const string clientId = "myclient"; var data = new[] { new KeyValuePair("client_id", clientId), new KeyValuePair("client_secret", "secret"), new KeyValuePair("response_type", "token"), new KeyValuePair("grant_type", "client_credentials"), }; if (useAuthenticationHeader) { // client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("basic", // Convert.ToBase64String($"{clientId}:{clientSecret}")) } var content = new FormUrlEncodedContent(data); var response = await client.PostAsync( "/auth/realms/foo/openid-connect/token", content, TestContext.Current.CancellationToken); // Verify // var responseJson = await response.Content.ReadAsStringAsync( // TestContext.Current.CancellationToken); // Console.WriteLine($"Response: {responseJson}"); response.StatusCode.ShouldBe(HttpStatusCode.OK); // Cache-Control: no-store response.Headers.CacheControl.ShouldNotBeNull() .NoStore.ShouldBe(true); // Pragma: no-cache response.Headers.Pragma.ShouldNotBeNull() .ShouldContain(new NameValueHeaderValue("no-cache")); var payload = await response.Content.ReadFromJsonAsync(); payload.ShouldNotBeNull(); Assert.Multiple( () => payload.AccessToken.ShouldNotBeNull(), () => payload.TokenType.ShouldBe("bearer"), () => payload.ExpiresIn.ShouldBe(3600)); // - refresh_token OPTIONAL // - scope OPTIONAL when identical to request otherwise REQUIRED } internal class TokenResponse { [JsonPropertyName("access_token")] public string? AccessToken { get; set; } [JsonPropertyName("token_type")] public string? TokenType { get; set; } [JsonPropertyName("expires_in")] public int? ExpiresIn { get; set; } } }