Skip to content

feat(auth): Support for creating tenant-scoped session cookies #233

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Aug 18, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 73 additions & 31 deletions FirebaseAdmin/FirebaseAdmin.Tests/Auth/FirebaseUserManagerTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
using System.Net.Http;
using System.Threading.Tasks;
using FirebaseAdmin.Auth.Jwt;
using FirebaseAdmin.Auth.Jwt.Tests;
using FirebaseAdmin.Auth.Multitenancy;
using FirebaseAdmin.Tests;
using FirebaseAdmin.Util;
using Google.Apis.Json;
Expand Down Expand Up @@ -1759,12 +1761,12 @@ public void RevokeRefreshTokensInvalidUid(TestConfig config)
async () => await auth.RevokeRefreshTokensAsync(uid));
}

[Fact]
public void CreateSessionCookieNoIdToken()
[Theory]
[MemberData(nameof(TestConfigs))]
public void CreateSessionCookieNoIdToken(TestConfig config)
{
var config = TestConfig.ForFirebaseAuth();
var handler = new MockMessageHandler() { Response = "{}" };
var auth = (FirebaseAuth)config.CreateAuth(handler);
var auth = config.CreateAuthWithIdTokenVerifier(handler);
var options = new SessionCookieOptions()
{
ExpiresIn = TimeSpan.FromHours(1),
Expand All @@ -1776,89 +1778,110 @@ public void CreateSessionCookieNoIdToken()
async () => await auth.CreateSessionCookieAsync(string.Empty, options));
}

[Fact]
public void CreateSessionCookieNoOptions()
[Theory]
[MemberData(nameof(TestConfigs))]
public void CreateSessionCookieNoOptions(TestConfig config)
{
var config = TestConfig.ForFirebaseAuth();
var handler = new MockMessageHandler() { Response = "{}" };
var auth = (FirebaseAuth)config.CreateAuth(handler);
var auth = config.CreateAuth(handler);

Assert.ThrowsAsync<ArgumentNullException>(
async () => await auth.CreateSessionCookieAsync("idToken", null));
}

[Fact]
public void CreateSessionCookieNoExpiresIn()
[Theory]
[MemberData(nameof(TestConfigs))]
public void CreateSessionCookieNoExpiresIn(TestConfig config)
{
var config = TestConfig.ForFirebaseAuth();
var handler = new MockMessageHandler() { Response = "{}" };
var auth = (FirebaseAuth)config.CreateAuth(handler);
var auth = config.CreateAuth(handler);

Assert.ThrowsAsync<ArgumentException>(
async () => await auth.CreateSessionCookieAsync(
"idToken", new SessionCookieOptions()));
}

[Fact]
public void CreateSessionCookieExpiresInTooLow()
[Theory]
[MemberData(nameof(TestConfigs))]
public async Task CreateSessionCookieExpiresInTooLow(TestConfig config)
{
var config = TestConfig.ForFirebaseAuth();
var handler = new MockMessageHandler() { Response = "{}" };
var auth = (FirebaseAuth)config.CreateAuth(handler);
var auth = config.CreateAuth(handler);
var fiveMinutesInSeconds = TimeSpan.FromMinutes(5).TotalSeconds;
var options = new SessionCookieOptions()
{
ExpiresIn = TimeSpan.FromSeconds(fiveMinutesInSeconds - 1),
};

Assert.ThrowsAsync<ArgumentException>(
await Assert.ThrowsAsync<ArgumentException>(
async () => await auth.CreateSessionCookieAsync("idToken", options));
}

[Fact]
public void CreateSessionCookieExpiresInTooHigh()
[Theory]
[MemberData(nameof(TestConfigs))]
public async Task CreateSessionCookieExpiresInTooHigh(TestConfig config)
{
var config = TestConfig.ForFirebaseAuth();
var handler = new MockMessageHandler() { Response = "{}" };
var auth = (FirebaseAuth)config.CreateAuth(handler);
var auth = config.CreateAuth(handler);
var fourteenDaysInSeconds = TimeSpan.FromDays(14).TotalSeconds;
var options = new SessionCookieOptions()
{
ExpiresIn = TimeSpan.FromSeconds(fourteenDaysInSeconds + 1),
};

Assert.ThrowsAsync<ArgumentException>(
await Assert.ThrowsAsync<ArgumentException>(
async () => await auth.CreateSessionCookieAsync("idToken", options));
}

[Fact]
public async Task CreateSessionCookie()
[Theory]
[MemberData(nameof(TestConfigs))]
public async Task CreateSessionCookie(TestConfig config)
{
var config = TestConfig.ForFirebaseAuth();
var handler = new MockMessageHandler()
{
Response = @"{
""sessionCookie"": ""cookie""
}",
};
var auth = (FirebaseAuth)config.CreateAuth(handler);
var auth = config.CreateAuthWithIdTokenVerifier(handler);
var idToken = await CreateIdTokenAsync(config.TenantId);
var options = new SessionCookieOptions()
{
ExpiresIn = TimeSpan.FromHours(1),
};

var result = await auth.CreateSessionCookieAsync("idToken", options);
var result = await auth.CreateSessionCookieAsync(idToken, options);

Assert.Equal("cookie", result);
Assert.Equal(1, handler.Requests.Count);
var request = NewtonsoftJsonSerializer.Instance.Deserialize<JObject>(handler.LastRequestBody);
Assert.Equal(2, request.Count);
Assert.Equal("idToken", request["idToken"]);
Assert.Equal(idToken, request["idToken"]);
Assert.Equal(3600, request["validDuration"]);

config.AssertRequest(":createSessionCookie", Assert.Single(handler.Requests));
}

[Fact]
public async Task CreateSessionCookieTenantIdMismatch()
{
var config = TestConfig.ForTenantAwareFirebaseAuth("test-tenant");
var auth = (TenantAwareFirebaseAuth)config.CreateAuthWithIdTokenVerifier();
var idToken = await CreateIdTokenAsync("other-tenant");
var options = new SessionCookieOptions()
{
ExpiresIn = TimeSpan.FromHours(1),
};

var exception = await Assert.ThrowsAsync<FirebaseAuthException>(
() => auth.CreateSessionCookieAsync(idToken, options));

Assert.Equal(ErrorCode.InvalidArgument, exception.ErrorCode);
Assert.Equal(AuthErrorCode.TenantIdMismatch, exception.AuthErrorCode);
Assert.Null(exception.InnerException);
Assert.Null(exception.HttpResponse);
}

[Theory]
[MemberData(nameof(TestConfigs))]
public async Task ServiceUnvailable(TestConfig config)
Expand Down Expand Up @@ -1929,27 +1952,35 @@ private static FirebaseUserManager.Args CreateArgs()
};
}

private static async Task<string> CreateIdTokenAsync(string tenantId)
{
var tokenBuilder = JwtTestUtils.IdTokenBuilder(tenantId);
tokenBuilder.ProjectId = TestConfig.MockProjectId;
return await tokenBuilder.CreateTokenAsync();
}

public class TestConfig
{
internal const string MockProjectId = "project1";

internal static readonly IClock Clock = new MockClock();

private readonly string tenantId;
private readonly AuthBuilder authBuilder;

private TestConfig(string tenantId = null)
{
this.tenantId = tenantId;
this.authBuilder = new AuthBuilder
{
ProjectId = MockProjectId,
Clock = Clock,
RetryOptions = RetryOptions.NoBackOff,
KeySource = JwtTestUtils.DefaultKeySource,
TenantId = tenantId,
};
}

public string TenantId => this.authBuilder.TenantId;

public static TestConfig ForFirebaseAuth()
{
return new TestConfig();
Expand All @@ -1969,10 +2000,21 @@ public AbstractFirebaseAuth CreateAuth(HttpMessageHandler handler = null)
return this.authBuilder.Build(options);
}

public AbstractFirebaseAuth CreateAuthWithIdTokenVerifier(
HttpMessageHandler handler = null)
{
var options = new TestOptions
{
UserManagerRequestHandler = handler,
IdTokenVerifier = true,
};
return this.authBuilder.Build(options);
}

internal void AssertRequest(
string expectedSuffix, MockMessageHandler.IncomingRequest request)
{
var tenantInfo = this.tenantId != null ? $"/tenants/{this.tenantId}" : string.Empty;
var tenantInfo = this.TenantId != null ? $"/tenants/{this.TenantId}" : string.Empty;
var expectedPath = $"/v1/projects/{MockProjectId}{tenantInfo}/{expectedSuffix}";
Assert.Equal(expectedPath, request.Url.PathAndQuery);
}
Expand Down
33 changes: 33 additions & 0 deletions FirebaseAdmin/FirebaseAdmin/Auth/AbstractFirebaseAuth.cs
Original file line number Diff line number Diff line change
Expand Up @@ -978,6 +978,39 @@ public async Task<string> GenerateSignInWithEmailLinkAsync(
.ConfigureAwait(false);
}

/// <summary>
/// Creates a new Firebase session cookie from the given ID token and options. The returned JWT can
/// be set as a server-side session cookie with a custom cookie policy.
/// </summary>
/// <exception cref="FirebaseAuthException">If an error occurs while creating the cookie.</exception>
/// <param name="idToken">The Firebase ID token to exchange for a session cookie.</param>
/// <param name="options">Additional options required to create the cookie.</param>
/// <returns>A task that completes with the Firebase session cookie.</returns>
public async Task<string> CreateSessionCookieAsync(
string idToken, SessionCookieOptions options)
{
return await this.CreateSessionCookieAsync(idToken, options, default(CancellationToken))
.ConfigureAwait(false);
}

/// <summary>
/// Creates a new Firebase session cookie from the given ID token and options. The returned JWT can
/// be set as a server-side session cookie with a custom cookie policy.
/// </summary>
/// <exception cref="FirebaseAuthException">If an error occurs while creating the cookie.</exception>
/// <param name="idToken">The Firebase ID token to exchange for a session cookie.</param>
/// <param name="options">Additional options required to create the cookie.</param>
/// <param name="cancellationToken">A cancellation token to monitor the asynchronous
/// operation.</param>
/// <returns>A task that completes with the Firebase session cookie.</returns>
public virtual async Task<string> CreateSessionCookieAsync(
string idToken, SessionCookieOptions options, CancellationToken cancellationToken)
{
return await this.UserManager
.CreateSessionCookieAsync(idToken, options, cancellationToken)
.ConfigureAwait(false);
}

/// <summary>
/// Parses and verifies a Firebase session cookie.
///
Expand Down
33 changes: 0 additions & 33 deletions FirebaseAdmin/FirebaseAdmin/Auth/FirebaseAuth.cs
Original file line number Diff line number Diff line change
Expand Up @@ -83,39 +83,6 @@ public static FirebaseAuth GetAuth(FirebaseApp app)
});
}

/// <summary>
/// Creates a new Firebase session cookie from the given ID token and options. The returned JWT can
/// be set as a server-side session cookie with a custom cookie policy.
/// </summary>
/// <exception cref="FirebaseAuthException">If an error occurs while creating the cookie.</exception>
/// <param name="idToken">The Firebase ID token to exchange for a session cookie.</param>
/// <param name="options">Additional options required to create the cookie.</param>
/// <returns>A task that completes with the Firebase session cookie.</returns>
public async Task<string> CreateSessionCookieAsync(
string idToken, SessionCookieOptions options)
{
return await this.CreateSessionCookieAsync(idToken, options, default(CancellationToken))
.ConfigureAwait(false);
}

/// <summary>
/// Creates a new Firebase session cookie from the given ID token and options. The returned JWT can
/// be set as a server-side session cookie with a custom cookie policy.
/// </summary>
/// <exception cref="FirebaseAuthException">If an error occurs while creating the cookie.</exception>
/// <param name="idToken">The Firebase ID token to exchange for a session cookie.</param>
/// <param name="options">Additional options required to create the cookie.</param>
/// <param name="cancellationToken">A cancellation token to monitor the asynchronous
/// operation.</param>
/// <returns>A task that completes with the Firebase session cookie.</returns>
public async Task<string> CreateSessionCookieAsync(
string idToken, SessionCookieOptions options, CancellationToken cancellationToken)
{
return await this.UserManager
.CreateSessionCookieAsync(idToken, options, cancellationToken)
.ConfigureAwait(false);
}

/// <summary>
/// Looks up an OIDC auth provider configuration by the provided ID.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@
// limitations under the License.

using System;
using System.Threading;
using System.Threading.Tasks;
using FirebaseAdmin.Auth.Jwt;
using Google.Apis.Util;

namespace FirebaseAdmin.Auth.Multitenancy
{
Expand All @@ -38,6 +41,17 @@ internal TenantAwareFirebaseAuth(Args args)
/// </summary>
public string TenantId { get; }

/// <inheritdoc/>
public override async Task<string> CreateSessionCookieAsync(
string idToken, SessionCookieOptions options, CancellationToken cancellationToken)
{
// As a minor optimization, validate options here before calling VerifyIdToken().
options.ThrowIfNull(nameof(options)).CopyAndValidate();
await this.VerifyIdTokenAsync(idToken, cancellationToken);
return await base.CreateSessionCookieAsync(idToken, options, cancellationToken)
.ConfigureAwait(false);
}

internal static TenantAwareFirebaseAuth Create(FirebaseApp app, string tenantId)
{
var args = new Args
Expand Down
4 changes: 3 additions & 1 deletion FirebaseAdmin/FirebaseAdmin/Auth/SessionCookieOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@
namespace FirebaseAdmin.Auth
{
/// <summary>
/// Options for the <see cref="FirebaseAuth.CreateSessionCookieAsync(string, SessionCookieOptions)"/> API.
/// Options for the
/// <see cref="AbstractFirebaseAuth.CreateSessionCookieAsync(string, SessionCookieOptions)"/>
/// API.
/// </summary>
public sealed class SessionCookieOptions
{
Expand Down