Skip to content

Commit d3541a8

Browse files
authored
Add GetCurrentUser method to the repository API. (#113)
* Add GetCurrentUser method to the repository API. * Add more tests and fallback to visitor.
1 parent ede96ad commit d3541a8

File tree

3 files changed

+268
-0
lines changed

3 files changed

+268
-0
lines changed

src/SenseNet.Client.Tests/UnitTests/RepositoryTests.cs

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1956,6 +1956,202 @@ private async Task QueryForAdminTest<T>(bool isExceptionExpected) where T : Cont
19561956
Assert.AreEqual("Item1, Item2, Item3, Item4", typeNames);
19571957
}
19581958

1959+
/* ============================================================================ AUTHENTICATION */
1960+
1961+
[TestMethod]
1962+
public async Task Repository_Auth_GetCurrentUser_ValidUser_ValidToken()
1963+
{
1964+
// ALIGN
1965+
var restCaller = CreateRestCallerFor(@"{ ""d"": {
1966+
""Name"": ""Admin"", ""Id"": 1, ""Type"": ""User"" }}");
1967+
var repositories = GetRepositoryCollection(services =>
1968+
{
1969+
services.AddSingleton(restCaller);
1970+
});
1971+
var repository = await repositories.GetRepositoryAsync("local", CancellationToken.None)
1972+
.ConfigureAwait(false);
1973+
1974+
// this is a test token containing the admin id (1) as a SUB
1975+
repository.Server.Authentication.AccessToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxIiwibm" +
1976+
"FtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.ZU43TYZENiuL" +
1977+
"dKJPpd-hnkFhRkpLPurixsKr-8m-kBc";
1978+
1979+
// ACT
1980+
var content = await repository.GetCurrentUserAsync(CancellationToken.None)
1981+
.ConfigureAwait(false);
1982+
1983+
// ASSERT
1984+
var requestedUri = (Uri)restCaller.ReceivedCalls().ToArray()[1].GetArguments().First()!;
1985+
Assert.IsNotNull(requestedUri);
1986+
Assert.AreEqual("/OData.svc/content(1)?metadata=no", requestedUri.PathAndQuery);
1987+
1988+
Assert.IsNotNull(content);
1989+
Assert.AreEqual("Admin", content.Name);
1990+
}
1991+
[TestMethod]
1992+
public async Task Repository_Auth_GetCurrentUser_ValidUser_ValidToken_WithParameters()
1993+
{
1994+
// ALIGN
1995+
var restCaller = CreateRestCallerFor(@"{ ""d"": {
1996+
""Name"": ""Admin"", ""Id"": 1, ""Type"": ""User"" }}");
1997+
var repositories = GetRepositoryCollection(services =>
1998+
{
1999+
services.AddSingleton(restCaller);
2000+
});
2001+
var repository = await repositories.GetRepositoryAsync("local", CancellationToken.None)
2002+
.ConfigureAwait(false);
2003+
2004+
// this is a test token containing the admin id (1) as a SUB
2005+
repository.Server.Authentication.AccessToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxIiwibm" +
2006+
"FtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.ZU43TYZENiuL" +
2007+
"dKJPpd-hnkFhRkpLPurixsKr-8m-kBc";
2008+
2009+
// ACT
2010+
// define select and expand parameters
2011+
var content = await repository.GetCurrentUserAsync(
2012+
new []{"Id", "Name", "Type", "Manager/Name"},
2013+
new []{"Manager"},
2014+
CancellationToken.None).ConfigureAwait(false);
2015+
2016+
// ASSERT
2017+
var requestedUri = (Uri)restCaller.ReceivedCalls().ToArray()[1].GetArguments().First()!;
2018+
Assert.IsNotNull(requestedUri);
2019+
Assert.AreEqual("/OData.svc/content(1)?metadata=no&$expand=Manager&$select=Id,Name,Type,Manager/Name",
2020+
requestedUri.PathAndQuery);
2021+
2022+
Assert.IsNotNull(content);
2023+
Assert.AreEqual("Admin", content.Name);
2024+
}
2025+
[TestMethod]
2026+
public async Task Repository_Auth_GetCurrentUser_ValidUser_ExpiredToken()
2027+
{
2028+
// ALIGN
2029+
var restCaller = Substitute.For<IRestCaller>();
2030+
2031+
// first call: expired token, inaccessible user id
2032+
restCaller
2033+
.GetResponseStringAsync(Arg.Is<Uri>(uri => uri.PathAndQuery.Contains("/OData.svc/content(123456)")),
2034+
Arg.Any<HttpMethod>(), Arg.Any<string>(),
2035+
Arg.Any<Dictionary<string, IEnumerable<string>>>(),
2036+
Arg.Any<CancellationToken>())
2037+
.Returns(Task.FromResult(string.Empty));
2038+
2039+
// second call: GetCurrentUser action, returns the Visitor user
2040+
restCaller.GetResponseStringAsync(Arg.Is<Uri>(uri => uri.PathAndQuery.Contains("/OData.svc/('Root')/GetCurrentUser")),
2041+
Arg.Any<HttpMethod>(), Arg.Any<string>(),
2042+
Arg.Any<Dictionary<string, IEnumerable<string>>>(),
2043+
Arg.Any<CancellationToken>())
2044+
.Returns(Task.FromResult(@"{ ""Name"": ""Visitor"", ""Id"": 6, ""Type"": ""User"" }"));
2045+
2046+
var repositories = GetRepositoryCollection(services =>
2047+
{
2048+
services.AddSingleton(restCaller);
2049+
});
2050+
var repository = await repositories.GetRepositoryAsync("local", CancellationToken.None)
2051+
.ConfigureAwait(false);
2052+
2053+
// this is a test token containing 123456 (INACCESSIBLE user) as a SUB
2054+
repository.Server.Authentication.AccessToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMj" +
2055+
"M0NTYiLCJuYW1lIjoiSm9obiBEb2UiLCJpYXQiOjE1MTYyMzkwM" +
2056+
"jJ9.MkiS50WhvOFwrwxQzd5Kp3VzkQUZhvex3kQv-CLeS3M";
2057+
2058+
// ACT
2059+
var content = await repository.GetCurrentUserAsync(CancellationToken.None)
2060+
.ConfigureAwait(false);
2061+
2062+
// ASSERT
2063+
var requestedUri1 = (Uri)restCaller.ReceivedCalls().ToArray()[1].GetArguments().First()!;
2064+
Assert.AreEqual("/OData.svc/content(123456)?metadata=no", requestedUri1.PathAndQuery);
2065+
2066+
var requestedUri2 = (Uri)restCaller.ReceivedCalls().ToArray()[2].GetArguments().First()!;
2067+
Assert.AreEqual("/OData.svc/('Root')/GetCurrentUser?metadata=no", requestedUri2.PathAndQuery);
2068+
2069+
Assert.IsNotNull(content);
2070+
Assert.AreEqual("Visitor", content.Name);
2071+
}
2072+
[TestMethod]
2073+
public async Task Repository_Auth_GetCurrentUser_ValidUser_UnknownToken()
2074+
{
2075+
// ALIGN
2076+
var restCaller = CreateRestCallerFor(@"{""Name"": ""Admin"", ""Id"": 1, ""Type"": ""User"" }");
2077+
var repositories = GetRepositoryCollection(services =>
2078+
{
2079+
services.AddSingleton(restCaller);
2080+
});
2081+
var repository = await repositories.GetRepositoryAsync("local", CancellationToken.None)
2082+
.ConfigureAwait(false);
2083+
2084+
// edge case: this is a not parseable token that is still accepted by the server
2085+
repository.Server.Authentication.AccessToken = "not parseable token";
2086+
2087+
// ACT
2088+
var content = await repository.GetCurrentUserAsync(CancellationToken.None)
2089+
.ConfigureAwait(false);
2090+
2091+
// ASSERT
2092+
var requestedUri = (Uri)restCaller.ReceivedCalls().ToArray()[1].GetArguments().First()!;
2093+
Assert.IsNotNull(requestedUri);
2094+
Assert.AreEqual("/OData.svc/('Root')/GetCurrentUser?metadata=no", requestedUri.PathAndQuery);
2095+
2096+
Assert.IsNotNull(content);
2097+
Assert.AreEqual("Admin", content.Name);
2098+
}
2099+
[TestMethod]
2100+
public async Task Repository_Auth_GetCurrentUser_ValidUser_ApiKey()
2101+
{
2102+
// ALIGN
2103+
var restCaller = CreateRestCallerFor(@"{""Name"": ""Admin"", ""Id"": 1, ""Type"": ""User"" }");
2104+
var repositories = GetRepositoryCollection(services =>
2105+
{
2106+
services.AddSingleton(restCaller);
2107+
});
2108+
var repository = await repositories.GetRepositoryAsync("local", CancellationToken.None)
2109+
.ConfigureAwait(false);
2110+
2111+
// we provide an api key instead of an access token
2112+
repository.Server.Authentication.ApiKey = "valid api key";
2113+
2114+
// ACT
2115+
var content = await repository.GetCurrentUserAsync(CancellationToken.None)
2116+
.ConfigureAwait(false);
2117+
2118+
// ASSERT
2119+
var requestedUri = (Uri)restCaller.ReceivedCalls().ToArray()[1].GetArguments().First()!;
2120+
Assert.IsNotNull(requestedUri);
2121+
Assert.AreEqual("/OData.svc/('Root')/GetCurrentUser?metadata=no", requestedUri.PathAndQuery);
2122+
2123+
Assert.IsNotNull(content);
2124+
Assert.AreEqual("Admin", content.Name);
2125+
}
2126+
[TestMethod]
2127+
public async Task Repository_Auth_GetCurrentUser_Visitor_NoToken()
2128+
{
2129+
// ALIGN
2130+
var restCaller = CreateRestCallerFor(@"{ ""d"": {
2131+
""Name"": ""Visitor"", ""Id"": 6, ""Type"": ""User"" }}");
2132+
var repositories = GetRepositoryCollection(services =>
2133+
{
2134+
services.AddSingleton(restCaller);
2135+
});
2136+
var repository = await repositories.GetRepositoryAsync("local", CancellationToken.None)
2137+
.ConfigureAwait(false);
2138+
2139+
// no token
2140+
repository.Server.Authentication.AccessToken = null;
2141+
2142+
// ACT
2143+
var content = await repository.GetCurrentUserAsync(CancellationToken.None)
2144+
.ConfigureAwait(false);
2145+
2146+
// ASSERT
2147+
var requestedUri = (Uri)restCaller.ReceivedCalls().ToArray()[1].GetArguments().First()!;
2148+
Assert.IsNotNull(requestedUri);
2149+
Assert.AreEqual("/OData.svc/Root/IMS/BuiltIn/Portal('Visitor')?metadata=no", requestedUri.PathAndQuery);
2150+
2151+
Assert.IsNotNull(content);
2152+
Assert.AreEqual("Visitor", content.Name);
2153+
}
2154+
19592155
/* ====================================================================== CUSTOM REQUESTS */
19602156

19612157
private class CustomNestedObject

src/SenseNet.Client/Repository/IRepository.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,11 @@ public interface IRepository
393393
/// <returns>A task that represents an asynchronous operation.</returns>
394394
public Task DeleteContentAsync(object[] idsOrPaths, bool permanent, CancellationToken cancel);
395395

396+
/* ============================================================================ AUTHENTICATION */
397+
398+
public Task<Content> GetCurrentUserAsync(CancellationToken cancel);
399+
public Task<Content> GetCurrentUserAsync(string[] select, string[] expand, CancellationToken cancel);
400+
396401
/* ============================================================================ MIDDLE LEVEL API */
397402

398403
/// <summary>

src/SenseNet.Client/Repository/Repository.cs

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
using System.Net.Http.Headers;
1414
using System.Net;
1515
using System.Text;
16+
using System.IdentityModel.Tokens.Jwt;
1617

1718
// ReSharper disable once CheckNamespace
1819
namespace SenseNet.Client;
@@ -565,6 +566,72 @@ await GetResponseStringAsync(oDataRequest, HttpMethod.Post, cancel)
565566
: $"{idsOrPaths.Length} contents were deleted.");
566567
}
567568

569+
/* ============================================================================ AUTHENTICATION */
570+
571+
public Task<Content> GetCurrentUserAsync(CancellationToken cancel)
572+
{
573+
return GetCurrentUserAsync(null, null, cancel);
574+
}
575+
public async Task<Content> GetCurrentUserAsync(string[] select, string[] expand, CancellationToken cancel)
576+
{
577+
var accessToken = Server?.Authentication?.AccessToken;
578+
579+
if (!string.IsNullOrEmpty(accessToken))
580+
{
581+
// The token contains the user id in the SUB claim.
582+
try
583+
{
584+
var handler = new JwtSecurityTokenHandler();
585+
var jwtSecurityToken = handler.ReadJwtToken(accessToken);
586+
587+
if (int.TryParse(jwtSecurityToken.Subject, out var contentId))
588+
{
589+
// Userid found: simply load the user. This will make this method work
590+
// even with older portals where the action below does not exist yet.
591+
var user = await LoadContentAsync(new LoadContentRequest
592+
{
593+
ContentId = contentId,
594+
Select = select,
595+
Expand = expand
596+
}, cancel).ConfigureAwait(false);
597+
598+
if (user != null)
599+
return user;
600+
601+
// the user is not found, we will load the visitor later
602+
}
603+
}
604+
catch (Exception ex)
605+
{
606+
_logger.LogTrace(ex, "Error during JWT access token conversion.");
607+
}
608+
}
609+
610+
// if there is a chance that the user is authenticated (token or key is present)
611+
if (!string.IsNullOrEmpty(accessToken) || !string.IsNullOrEmpty(Server?.Authentication?.ApiKey))
612+
{
613+
// we could not extract the user from the token: use the new action as a fallback
614+
var request = new ODataRequest(Server)
615+
{
616+
Path = "/Root",
617+
ActionName = "GetCurrentUser",
618+
Select = select,
619+
Expand = expand
620+
};
621+
622+
var response = await GetResponseJsonAsync(request, HttpMethod.Get, cancel).ConfigureAwait(false);
623+
return CreateContentFromJson(response);
624+
}
625+
626+
// no token: load Visitor
627+
return await LoadContentAsync(new LoadContentRequest
628+
{
629+
Path = Constants.User.VisitorPath,
630+
Select = select,
631+
Expand = expand
632+
}, cancel).ConfigureAwait(false);
633+
}
634+
568635
/* ============================================================================ MIDDLE LEVEL API */
569636

570637
public async Task<T> GetResponseAsync<T>(ODataRequest requestData, HttpMethod method, CancellationToken cancel)

0 commit comments

Comments
 (0)