Skip to content

Commit 754a89e

Browse files
authored
feat(auth): Added CreateSessionCookieAsync() API (#176)
* feat(auth): Added CreateSessionCookieAsync() API * Added newline at eof
1 parent 4a331b4 commit 754a89e

File tree

4 files changed

+201
-0
lines changed

4 files changed

+201
-0
lines changed

FirebaseAdmin/FirebaseAdmin.Tests/Auth/FirebaseUserManagerTest.cs

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1575,6 +1575,100 @@ public void RevokeRefreshTokensInvalidUid()
15751575
async () => await auth.RevokeRefreshTokensAsync(uid));
15761576
}
15771577

1578+
[Fact]
1579+
public void CreateSessionCookieNoIdToken()
1580+
{
1581+
var handler = new MockMessageHandler() { Response = "{}" };
1582+
var auth = this.CreateFirebaseAuth(handler);
1583+
var options = new SessionCookieOptions()
1584+
{
1585+
ExpiresIn = TimeSpan.FromHours(1),
1586+
};
1587+
1588+
Assert.ThrowsAsync<ArgumentException>(
1589+
async () => await auth.CreateSessionCookieAsync(null, options));
1590+
Assert.ThrowsAsync<ArgumentException>(
1591+
async () => await auth.CreateSessionCookieAsync(string.Empty, options));
1592+
}
1593+
1594+
[Fact]
1595+
public void CreateSessionCookieNoOptions()
1596+
{
1597+
var handler = new MockMessageHandler() { Response = "{}" };
1598+
var auth = this.CreateFirebaseAuth(handler);
1599+
1600+
Assert.ThrowsAsync<ArgumentNullException>(
1601+
async () => await auth.CreateSessionCookieAsync("idToken", null));
1602+
}
1603+
1604+
[Fact]
1605+
public void CreateSessionCookieNoExpiresIn()
1606+
{
1607+
var handler = new MockMessageHandler() { Response = "{}" };
1608+
var auth = this.CreateFirebaseAuth(handler);
1609+
1610+
Assert.ThrowsAsync<ArgumentException>(
1611+
async () => await auth.CreateSessionCookieAsync(
1612+
"idToken", new SessionCookieOptions()));
1613+
}
1614+
1615+
[Fact]
1616+
public void CreateSessionCookieExpiresInTooLow()
1617+
{
1618+
var handler = new MockMessageHandler() { Response = "{}" };
1619+
var auth = this.CreateFirebaseAuth(handler);
1620+
var fiveMinutesInSeconds = TimeSpan.FromMinutes(5).TotalSeconds;
1621+
var options = new SessionCookieOptions()
1622+
{
1623+
ExpiresIn = TimeSpan.FromSeconds(fiveMinutesInSeconds - 1),
1624+
};
1625+
1626+
Assert.ThrowsAsync<ArgumentException>(
1627+
async () => await auth.CreateSessionCookieAsync("idToken", options));
1628+
}
1629+
1630+
[Fact]
1631+
public void CreateSessionCookieExpiresInTooHigh()
1632+
{
1633+
var handler = new MockMessageHandler() { Response = "{}" };
1634+
var auth = this.CreateFirebaseAuth(handler);
1635+
var fourteenDaysInSeconds = TimeSpan.FromDays(14).TotalSeconds;
1636+
var options = new SessionCookieOptions()
1637+
{
1638+
ExpiresIn = TimeSpan.FromSeconds(fourteenDaysInSeconds + 1),
1639+
};
1640+
1641+
Assert.ThrowsAsync<ArgumentException>(
1642+
async () => await auth.CreateSessionCookieAsync("idToken", options));
1643+
}
1644+
1645+
[Fact]
1646+
public async Task CreateSessionCookie()
1647+
{
1648+
var handler = new MockMessageHandler()
1649+
{
1650+
Response = @"{
1651+
""sessionCookie"": ""cookie""
1652+
}",
1653+
};
1654+
var auth = this.CreateFirebaseAuth(handler);
1655+
var options = new SessionCookieOptions()
1656+
{
1657+
ExpiresIn = TimeSpan.FromHours(1),
1658+
};
1659+
1660+
var result = await auth.CreateSessionCookieAsync("idToken", options);
1661+
1662+
Assert.Equal("cookie", result);
1663+
Assert.Equal(1, handler.Requests.Count);
1664+
var request = NewtonsoftJsonSerializer.Instance.Deserialize<JObject>(handler.LastRequestBody);
1665+
Assert.Equal(2, request.Count);
1666+
Assert.Equal("idToken", request["idToken"]);
1667+
Assert.Equal(3600, request["validDuration"]);
1668+
1669+
this.AssertClientVersion(handler.LastRequestHeaders);
1670+
}
1671+
15781672
[Fact]
15791673
public async Task ServiceUnvailable()
15801674
{

FirebaseAdmin/FirebaseAdmin/Auth/FirebaseAuth.cs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -811,6 +811,39 @@ public async Task<string> GenerateSignInWithEmailLinkAsync(
811811
.ConfigureAwait(false);
812812
}
813813

814+
/// <summary>
815+
/// Creates a new Firebase session cookie from the given ID token and options. The returned JWT can
816+
/// be set as a server-side session cookie with a custom cookie policy.
817+
/// </summary>
818+
/// <exception cref="FirebaseAuthException">If an error occurs while creating the cookie.</exception>
819+
/// <param name="idToken">The Firebase ID token to exchange for a session cookie.</param>
820+
/// <param name="options">Additional options required to create the cookie.</param>
821+
/// <returns>A task that completes with the Firebase session cookie.</returns>
822+
public async Task<string> CreateSessionCookieAsync(
823+
string idToken, SessionCookieOptions options)
824+
{
825+
return await this.CreateSessionCookieAsync(idToken, options, default(CancellationToken))
826+
.ConfigureAwait(false);
827+
}
828+
829+
/// <summary>
830+
/// Creates a new Firebase session cookie from the given ID token and options. The returned JWT can
831+
/// be set as a server-side session cookie with a custom cookie policy.
832+
/// </summary>
833+
/// <exception cref="FirebaseAuthException">If an error occurs while creating the cookie.</exception>
834+
/// <param name="idToken">The Firebase ID token to exchange for a session cookie.</param>
835+
/// <param name="options">Additional options required to create the cookie.</param>
836+
/// <param name="cancellationToken">A cancellation token to monitor the asynchronous
837+
/// operation.</param>
838+
/// <returns>A task that completes with the Firebase session cookie.</returns>
839+
public async Task<string> CreateSessionCookieAsync(
840+
string idToken, SessionCookieOptions options, CancellationToken cancellationToken)
841+
{
842+
var userManager = this.IfNotDeleted(() => this.userManager.Value);
843+
return await userManager.CreateSessionCookieAsync(idToken, options, cancellationToken)
844+
.ConfigureAwait(false);
845+
}
846+
814847
/// <summary>
815848
/// Deletes this <see cref="FirebaseAuth"/> service instance.
816849
/// </summary>

FirebaseAdmin/FirebaseAdmin/Auth/FirebaseUserManager.cs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,32 @@ internal async Task<string> GenerateEmailActionLinkAsync(
272272
return (string)response.Result["oobLink"];
273273
}
274274

275+
internal async Task<string> CreateSessionCookieAsync(
276+
string idToken,
277+
SessionCookieOptions options,
278+
CancellationToken cancellationToken = default(CancellationToken))
279+
{
280+
if (string.IsNullOrEmpty(idToken))
281+
{
282+
throw new ArgumentException("idToken must not be null or empty");
283+
}
284+
285+
var validOptions = options.ThrowIfNull(nameof(options)).CopyAndValidate();
286+
var request = new Dictionary<string, object>()
287+
{
288+
{ "idToken", idToken },
289+
{ "validDuration", (long)validOptions.ExpiresIn.TotalSeconds },
290+
};
291+
var response = await this.PostAndDeserializeAsync<JObject>(
292+
":createSessionCookie", request, cancellationToken).ConfigureAwait(false);
293+
if (response.Result == null || (string)response.Result["sessionCookie"] == null)
294+
{
295+
throw UnexpectedResponseException("Failed to generate session cookie");
296+
}
297+
298+
return (string)response.Result["sessionCookie"];
299+
}
300+
275301
private static FirebaseAuthException UnexpectedResponseException(
276302
string message, Exception inner = null, HttpResponseMessage resp = null)
277303
{
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// Copyright 2020, Google Inc. All rights reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
using System;
16+
17+
namespace FirebaseAdmin.Auth
18+
{
19+
/// <summary>
20+
/// Options for the <see cref="FirebaseAuth.CreateSessionCookieAsync(string, SessionCookieOptions)"/> API.
21+
/// </summary>
22+
public sealed class SessionCookieOptions
23+
{
24+
/// <summary>
25+
/// Gets or sets the duration until the cookie is expired. Must be between 5 minutes
26+
/// and 14 days. The backend service uses seconds precision for this parameter.
27+
/// </summary>
28+
public TimeSpan ExpiresIn { get; set; }
29+
30+
internal SessionCookieOptions CopyAndValidate()
31+
{
32+
var copy = new SessionCookieOptions()
33+
{
34+
ExpiresIn = this.ExpiresIn,
35+
};
36+
if (copy.ExpiresIn < TimeSpan.FromMinutes(5))
37+
{
38+
throw new ArgumentException("ExpiresIn must be at least 5 minutes");
39+
}
40+
else if (copy.ExpiresIn > TimeSpan.FromDays(14))
41+
{
42+
throw new ArgumentException("ExpiresIn must be at most 14 days");
43+
}
44+
45+
return copy;
46+
}
47+
}
48+
}

0 commit comments

Comments
 (0)