Skip to content

Commit

Permalink
Integration tests for oidc config
Browse files Browse the repository at this point in the history
  • Loading branch information
donaldgray committed Jan 19, 2024
1 parent b0491f3 commit fe04215
Show file tree
Hide file tree
Showing 7 changed files with 293 additions and 28 deletions.
2 changes: 1 addition & 1 deletion scripts/auth0/AddDlcsClaims.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ exports.onExecutePostLogin = async (event, api) => {

const dlcsType = metadata.dlcsType;
if (dlcsType && scopeRequested(event, typeScope)) {
api.idToken.setCustomClaim(typeClaim, dlcsRole);
api.idToken.setCustomClaim(typeClaim, dlcsType);
}
}

Expand Down
219 changes: 217 additions & 2 deletions src/IIIFAuth2/IIIFAuth2.API.Tests/Integration/AccessServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@
using FakeItEasy;
using IIIFAuth2.API.Data;
using IIIFAuth2.API.Data.Entities;
using IIIFAuth2.API.Infrastructure.Auth.RoleProvisioning.Oidc;
using IIIFAuth2.API.Models.Domain;
using IIIFAuth2.API.Tests.TestingInfrastructure;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;

Expand All @@ -18,14 +20,21 @@ public class AccessServiceTests : IClassFixture<AuthWebApplicationFactory>
{
private readonly HttpClient httpClient;
private readonly AuthServicesContext dbContext;
private static readonly IAuth0Client Auth0Client = A.Fake<IAuth0Client>();

public AccessServiceTests(AuthWebApplicationFactory factory, DatabaseFixture dbFixture)
{
dbContext = dbFixture.DbContext;
httpClient = factory
.WithConnectionString(dbFixture.ConnectionString)
.WithTestServices(services => services.AddSingleton(A.Fake<IAmazonSecretsManager>()))
.CreateClient();
.WithTestServices(services =>
services
.AddSingleton(A.Fake<IAmazonSecretsManager>())
.AddScoped<IAuth0Client>(_ => Auth0Client))
.CreateClient(new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false
});

dbFixture.CleanUp();
}
Expand Down Expand Up @@ -237,6 +246,212 @@ public async Task AccessService_Clickthrough_RendersWindowClose_IfSameHost()
label.TextContent.Should().Be("This window should close automatically...");
}

[Fact]
public async Task AccessService_Oidc_ReturnsRedirectToLoginUrl()
{
// Arrange
var path = $"/access/99/oidc?origin={httpClient.BaseAddress}";
var authUri = new Uri("http://sample.idp/authorize");
A.CallTo(() => Auth0Client.GetAuthLoginUrl(A<OidcConfiguration>._, A<AccessService>._, A<string>._))
.Returns(authUri);

// Act
var response = await httpClient.GetAsync(path);

// Assert
response.StatusCode.Should().Be(HttpStatusCode.Redirect);
response.Headers.Location.Should().Be(authUri);
}

[Fact]
public async Task AccessService_OAuth2Callback_Returns404_IfAccessServiceNotFound()
{
// Arrange
const string path = "/access/99/foo/oauth2/callback?state=value&code=12345";

// Act
var response = await httpClient.GetAsync(path);

// Assert
response.StatusCode.Should().Be(HttpStatusCode.NotFound);
}

[Fact]
public async Task AccessService_OAuth2Callback_Returns_WindowClose_IfStateUnknown()
{
// Arrange
const string path = "/access/99/oidc/oauth2/callback?state=value&code=12345";

// Act
var response = await httpClient.GetAsync(path);

// Assert
response.StatusCode.Should().Be(HttpStatusCode.OK);

var htmlParser = new HtmlParser();
var document = htmlParser.ParseDocument(await response.Content.ReadAsStreamAsync());
var label = document.QuerySelector("p:nth-child(2)") as IHtmlParagraphElement;
label.TextContent.Should().Be("This window should close automatically...");
}

[Fact]
public async Task AccessService_OAuth2Callback_Returns_WindowClose_IfStateUsed()
{
// Arrange
var token = new RoleProvisionToken
{
Id = ExpiringToken.GenerateNewToken(DateTime.UtcNow), Customer = 99, Used = true, Origin = "http://wherever"
};
await dbContext.RoleProvisionTokens.AddAsync(token);
await dbContext.SaveChangesAsync();
var path = $"/access/99/oidc/oauth2/callback?state={Uri.EscapeDataString(token.Id)}&code=12345";

// Act
var response = await httpClient.GetAsync(path);

// Assert
response.StatusCode.Should().Be(HttpStatusCode.OK);

var htmlParser = new HtmlParser();
var document = htmlParser.ParseDocument(await response.Content.ReadAsStreamAsync());
var label = document.QuerySelector("p:nth-child(2)") as IHtmlParagraphElement;
label.TextContent.Should().Be("This window should close automatically...");
}

[Fact]
public async Task AccessService_OAuth2Callback_RendersSignificantGestureView_MarksStateTokenAsUsed()
{
// Arrange
var stateToken = new RoleProvisionToken
{
Id = ExpiringToken.GenerateNewToken(DateTime.UtcNow), Customer = 99, Used = false, Origin = "http://whatever.here"
};
await dbContext.RoleProvisionTokens.AddAsync(stateToken);
await dbContext.SaveChangesAsync();
const string code =
nameof(AccessService_OAuth2Callback_RendersSignificantGestureView_WithRoleProvisionToken_IfDifferentHost);
var path = $"/access/99/oidc/oauth2/callback?state={Uri.EscapeDataString(stateToken.Id)}&code={code}";
A.CallTo(() =>
Auth0Client.GetDlcsRolesForCode(A<OidcConfiguration>._, A<AccessService>._, code,
A<CancellationToken>._))
.Returns(new List<string> { DatabaseFixture.OidcRoleUri });

// Act
var response = await httpClient.GetAsync(path);

// Assert
response.StatusCode.Should().Be(HttpStatusCode.OK);
await dbContext.Entry(stateToken).ReloadAsync();
stateToken.Used.Should().BeTrue();
}

[Fact]
public async Task AccessService_OAuth2Callback_RendersSignificantGestureView_WithRoleProvisionToken_IfDifferentHost()
{
// Arrange
var stateToken = new RoleProvisionToken
{
Id = ExpiringToken.GenerateNewToken(DateTime.UtcNow), Customer = 99, Used = false, Origin = "http://whatever.here"
};
await dbContext.RoleProvisionTokens.AddAsync(stateToken);
await dbContext.SaveChangesAsync();
const string code =
nameof(AccessService_OAuth2Callback_RendersSignificantGestureView_WithRoleProvisionToken_IfDifferentHost);
var path = $"/access/99/oidc/oauth2/callback?state={Uri.EscapeDataString(stateToken.Id)}&code={code}";
A.CallTo(() =>
Auth0Client.GetDlcsRolesForCode(A<OidcConfiguration>._, A<AccessService>._, code,
A<CancellationToken>._))
.Returns(new List<string> { DatabaseFixture.OidcRoleUri });

// Act
var response = await httpClient.GetAsync(path);

// Assert
response.StatusCode.Should().Be(HttpStatusCode.OK);

var htmlParser = new HtmlParser();
var document = htmlParser.ParseDocument(await response.Content.ReadAsStreamAsync());

var hidden = document.QuerySelector("form>#SingleUseToken") as IHtmlInputElement;
var hiddenValue = hidden!.Value;

ExpiringToken.HasExpired(hiddenValue).Should().BeFalse("A valid expiring token is returned");

var token = await dbContext.RoleProvisionTokens.SingleAsync(t => t.Id == hiddenValue);
token.Roles.Should().ContainSingle(DatabaseFixture.ClickthroughRoleUri);
token.Origin.Should().Be("http://whatever.here/");
token.Used.Should().BeFalse();
}

[Fact]
public async Task AccessService_OAuth2Callback_CreatesSessionAndSetsCookie_IfSameHost()
{
// Arrange
var stateToken = new RoleProvisionToken
{
Id = ExpiringToken.GenerateNewToken(DateTime.UtcNow), Customer = 99, Used = false, Origin = httpClient.BaseAddress.ToString()
};
await dbContext.RoleProvisionTokens.AddAsync(stateToken);
await dbContext.SaveChangesAsync();
const string code = nameof(AccessService_OAuth2Callback_CreatesSessionAndSetsCookie_IfSameHost);
var path = $"/access/99/oidc/oauth2/callback?state={Uri.EscapeDataString(stateToken.Id)}&code={code}";
A.CallTo(() =>
Auth0Client.GetDlcsRolesForCode(A<OidcConfiguration>._, A<AccessService>._, code,
A<CancellationToken>._))
.Returns(new List<string> { DatabaseFixture.OidcRoleUri });

// Act
var response = await httpClient.GetAsync(path);

// Assert
response.StatusCode.Should().Be(HttpStatusCode.OK);

response.Headers.Should().ContainKey("Set-Cookie");
var cookie = response.Headers.SingleOrDefault(header => header.Key == "Set-Cookie").Value.First();
cookie.Should()
.StartWith("dlcs-auth2-99")
.And.Contain("samesite=none")
.And.Contain("secure;")
.And.Contain("httponly");

// E.g. dlcs-token-99=id%3D76e7d9fb-99ab-4b4f-87b0-f2e3f0e9664e; expires=Tue, 14 Sep 2021 16:53:53 GMT; domain=localhost; path=/; secure; samesite=none
var toRemoveLength = "dlcs-auth2-99id%3D".Length;
var cookieId = cookie.Substring(toRemoveLength + 1, cookie.IndexOf(';') - toRemoveLength - 1);

var authToken = await dbContext.SessionUsers.SingleAsync(at => at.CookieId == cookieId);
authToken.Expires.Should().NotBeBefore(DateTime.UtcNow);
authToken.Customer.Should().Be(99);
}

[Fact]
public async Task AccessService_Oidc_RendersWindowClose_IfSameHost()
{
// Arrange
var stateToken = new RoleProvisionToken
{
Id = ExpiringToken.GenerateNewToken(DateTime.UtcNow), Customer = 99, Used = false, Origin = httpClient.BaseAddress.ToString()
};
await dbContext.RoleProvisionTokens.AddAsync(stateToken);
await dbContext.SaveChangesAsync();
const string code = nameof(AccessService_Oidc_RendersWindowClose_IfSameHost);
var path = $"/access/99/oidc/oauth2/callback?state={Uri.EscapeDataString(stateToken.Id)}&code={code}";
A.CallTo(() =>
Auth0Client.GetDlcsRolesForCode(A<OidcConfiguration>._, A<AccessService>._, code,
A<CancellationToken>._))
.Returns(new List<string> { DatabaseFixture.OidcRoleUri });

// Act
var response = await httpClient.GetAsync(path);

// Assert
response.StatusCode.Should().Be(HttpStatusCode.OK);

var htmlParser = new HtmlParser();
var document = htmlParser.ParseDocument(await response.Content.ReadAsStreamAsync());
var label = document.QuerySelector("p:nth-child(2)") as IHtmlParagraphElement;
label.TextContent.Should().Be("This window should close automatically...");
}

[Fact]
public async Task SignificantGesture_Returns400_IfNoSingleUseToken()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,13 @@ public class DatabaseFixture : IAsyncLifetime
public const int Customer = 99;
public const string CookieDomain = "localhost";
public const string ClickthroughService = "clickthrough";
public Guid AccessId;
public Guid RoleProviderId;
public const string OidcService = "oidc";
public Guid ClickthroughAccessId;
public Guid ClickthroughRoleProviderId;
public Guid OidcAccessId;
public Guid OidcRoleProviderId;
public const string ClickthroughRoleUri = "http://dlcs.test/99/clickthrough";
public const string OidcRoleUri = "http://dlcs.test/99/oidc";

public DatabaseFixture()
{
Expand Down Expand Up @@ -56,15 +60,15 @@ public async Task InitializeAsync()
public void CleanUp()
{
DbContext.Database.ExecuteSqlRaw("DELETE FROM session_users;");
DbContext.Database.ExecuteSqlRaw($"DELETE FROM roles WHERE access_service_id != '{AccessId.ToString()}';");
DbContext.Database.ExecuteSqlRaw($"DELETE FROM access_services WHERE id != '{AccessId.ToString()}';");
DbContext.Database.ExecuteSqlRaw($"DELETE FROM role_providers WHERE id != '{RoleProviderId.ToString()}';");
DbContext.Database.ExecuteSqlRaw($"DELETE FROM roles WHERE access_service_id not in ('{ClickthroughAccessId.ToString()}', '{OidcAccessId.ToString()}');");
DbContext.Database.ExecuteSqlRaw($"DELETE FROM access_services WHERE id not in ('{ClickthroughAccessId.ToString()}', '{OidcAccessId.ToString()}');");
DbContext.Database.ExecuteSqlRaw($"DELETE FROM role_providers WHERE id not in ('{ClickthroughRoleProviderId.ToString()}', '{OidcRoleProviderId.ToString()}');");
DbContext.Database.ExecuteSqlRaw($"DELETE FROM customer_cookie_domains WHERE customer != '{Customer}';");
}

private void SeedData()
{
var roleProvider = new RoleProvider
var clickthroughRoleProvider = new RoleProvider
{
Configuration = new RoleProviderConfiguration
{
Expand All @@ -74,34 +78,66 @@ private void SeedData()
}
}
};
DbContext.RoleProviders.Add(roleProvider);
var oidcRoleProvider = new RoleProvider
{
Configuration = new RoleProviderConfiguration
{
[RoleProviderConfiguration.DefaultKey] = new OidcConfiguration
{
Config = RoleProviderType.Oidc, GestureMessage = "Test-Message", GestureTitle = "Test-Title",
Provider = "auth0", Domain = "http://sample-domain.idp", Scopes = "test-scope",
ClientId = "clientId", ClientSecret = "secretsmanager:clientSecret",
UnknownValueBehaviour = UnknownMappingValueBehaviour.Fallback,
FallbackMapping = new[] { "OidcRoleUri" }
}
}
};
DbContext.RoleProviders.AddRange(clickthroughRoleProvider, oidcRoleProvider);

var accessService = new AccessService
var clickthroughAccessService = new AccessService
{
Customer = Customer,
Id = AccessId,
Id = ClickthroughAccessId,
Name = ClickthroughService,
Profile = "active",
RoleProvider = roleProvider
RoleProvider = clickthroughRoleProvider
};
DbContext.AccessServices.Add(accessService);
var oidcAccessService = new AccessService
{
Customer = Customer,
Id = OidcAccessId,
Name = OidcService,
Profile = "active",
RoleProvider = oidcRoleProvider
};
DbContext.AccessServices.AddRange(clickthroughAccessService, oidcAccessService);

DbContext.Roles.Add(new Role
DbContext.Roles.AddRange(new Role
{
Customer = Customer,
Id = ClickthroughRoleUri,
Name = "clickthrough-role",
AccessServiceId = accessService.Id
AccessServiceId = clickthroughAccessService.Id
}, new Role
{
Customer = Customer,
Id = OidcRoleUri,
Name = "oid-role",
AccessServiceId = oidcAccessService.Id
});
AccessId = accessService.Id;
RoleProviderId = accessService.RoleProviderId.Value;
ClickthroughAccessId = clickthroughAccessService.Id;
ClickthroughRoleProviderId = clickthroughAccessService.RoleProviderId.Value;
OidcAccessId = oidcAccessService.Id;
OidcRoleProviderId = oidcAccessService.RoleProviderId.Value;

DbContext.CustomerCookieDomains.Add(new CustomerCookieDomain
{
Customer = Customer,
Domains = new List<string> { CookieDomain }
});
DbContext.SaveChanges();

var x = DbContext.AccessServices;
}

public Task DisposeAsync() => postgresContainer.StopAsync();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,22 @@

namespace IIIFAuth2.API.Infrastructure.Auth.RoleProvisioning.Oidc;

public class Auth0Client
public interface IAuth0Client
{
/// <summary>
/// Get URI to redirect user for authorizing with auth0
/// </summary>
/// <remarks>See https://auth0.com/docs/api/authentication#-get-authorize- </remarks>
Uri GetAuthLoginUrl(OidcConfiguration oidcConfiguration, AccessService accessService, string state);

/// <summary>
/// Exchange authentication code for access tokens for logged in user
/// </summary>
Task<IReadOnlyCollection<string>> GetDlcsRolesForCode(OidcConfiguration oidcConfiguration,
AccessService accessService, string code, CancellationToken cancellationToken);
}

public class Auth0Client : IAuth0Client
{
private readonly IUrlPathProvider urlPathProvider;
private readonly HttpClient httpClient;
Expand Down
Loading

0 comments on commit fe04215

Please sign in to comment.