Skip to content

Commit c94feee

Browse files
authored
Merge pull request #144 from crhistianramirez/OrderCloudUserInfoAuth
Add UserInfoAuth attribute for securing API endpoints
2 parents 9898d66 + 75e4126 commit c94feee

20 files changed

Lines changed: 1343 additions & 417 deletions

OrderCloud.Catalyst.TestApi/Controllers/DemoController.cs

Lines changed: 51 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,25 @@
1+
using Microsoft.AspNetCore.Mvc;
2+
using OrderCloud.SDK;
3+
using Stripe;
14
using System;
25
using System.Collections.Generic;
36
using System.ComponentModel.DataAnnotations;
47
using System.Linq;
58
using System.Threading;
69
using System.Threading.Tasks;
7-
using Microsoft.AspNetCore.Mvc;
8-
using OrderCloud.SDK;
9-
using OrderCloud.Catalyst;
1010
using RequiredAttribute = System.ComponentModel.DataAnnotations.RequiredAttribute;
1111

1212
namespace OrderCloud.Catalyst.TestApi
1313
{
1414
[Route("demo")]
1515
public class DemoController : CatalystController
1616
{
17-
private readonly RequestAuthenticationService _tokenProvider;
17+
private readonly IRequestAuthenticationService _auth;
1818
private readonly IOrderCloudClient _oc;
1919

20-
public DemoController(RequestAuthenticationService tokenProvider, IOrderCloudClient oc)
20+
public DemoController(IRequestAuthenticationService auth, IOrderCloudClient oc)
2121
{
22-
_tokenProvider = tokenProvider;
22+
_auth = auth;
2323
_oc = oc;
2424
}
2525

@@ -28,7 +28,20 @@ public object Shop() {
2828
return "hello shopper!";
2929
}
3030

31-
[HttpGet("admin"), OrderCloudUserAuth(ApiRole.OrderAdmin)]
31+
[HttpGet("simpleuserinfo"), OrderCloudUserInfoAuth]
32+
public object SimpleUserInfo()
33+
{
34+
return "hello userinfo!";
35+
}
36+
37+
[HttpGet("customuserinfo"), OrderCloudUserInfoAuth("CustomRole")]
38+
public object CustomUserInfo()
39+
{
40+
return "hello custom userinfo!";
41+
}
42+
43+
44+
[HttpGet("admin"), OrderCloudUserAuth(ApiRole.OrderAdmin)]
3245
public object Admin() => "hello admin!";
3346

3447
[HttpGet("either"), OrderCloudUserAuth("Shopper", "OrderAdmin")]
@@ -94,21 +107,49 @@ public SimplifiedUser Username()
94107
};
95108
}
96109

97-
[HttpGet("username"), OrderCloudUserAuth]
110+
[HttpGet("userinfocontext"), OrderCloudUserInfoAuth]
111+
public SimplifiedUser GetUserInfoContext()
112+
{
113+
return new SimplifiedUser()
114+
{
115+
AvailableRoles = UserInfoContext.Roles.ToList(),
116+
Username = UserInfoContext.Username
117+
};
118+
}
119+
120+
[HttpPost("userinfocontext/{token}")]
121+
public async Task<SimplifiedUser> SetUserInfoContext(string token)
122+
{
123+
var user = await _auth.VerifyUserInfoTokenAsync(token);
124+
return new SimplifiedUser()
125+
{
126+
AvailableRoles = user.Roles.ToList(),
127+
Username = user.Username
128+
};
129+
}
130+
131+
[HttpGet("username"), OrderCloudUserAuth]
98132
public string GetUserName()
99133
{
100134
Thread.Sleep(1000); // pause for 1 sec
101135
return UserContext.Username;
102136
}
103137

104-
[HttpPost("usercontext/{token}")]
138+
[HttpGet("userinfousername"), OrderCloudUserInfoAuth]
139+
public string GetUserInfoUserName()
140+
{
141+
Thread.Sleep(1000); // pause for 1 sec
142+
return UserInfoContext.Username;
143+
}
144+
145+
[HttpPost("usercontext/{token}")]
105146
public async Task<SimplifiedUser> SetUserContext(string token)
106147
{
107148
var opts = new OrderCloudUserAuthOptions()
108149
{
109150
AnyClientIDCanAccess = true
110151
};
111-
var user = await _tokenProvider.VerifyTokenAsync(token, opts);
152+
var user = await _auth.VerifyTokenAsync(token, opts);
112153
return new SimplifiedUser() {
113154
AvailableRoles = user.Roles.ToList(),
114155
Username = user.Username,

OrderCloud.Catalyst.TestApi/Controllers/WebhookController.cs

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,15 @@
1-
using Microsoft.AspNetCore.Http;
2-
using Microsoft.AspNetCore.Mvc;
3-
using OrderCloud.Catalyst;
1+
using Microsoft.AspNetCore.Mvc;
42
using OrderCloud.SDK;
53
using System.Threading.Tasks;
64

75
namespace OrderCloud.Catalyst.TestApi
86
{
97
public class WebhookController : CatalystController
108
{
11-
private RequestAuthenticationService _service;
9+
private IRequestAuthenticationService _service;
1210
private TestSettings _settings;
1311

14-
public WebhookController(RequestAuthenticationService service, TestSettings settings)
12+
public WebhookController(IRequestAuthenticationService service, TestSettings settings)
1513
{
1614
_service = service;
1715
_settings = settings;

OrderCloud.Catalyst.TestApi/Startup.cs

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
using Microsoft.AspNetCore.Builder;
22
using Microsoft.AspNetCore.Hosting;
3+
using Microsoft.AspNetCore.Http;
4+
using Microsoft.AspNetCore.Mvc;
35
using Microsoft.Extensions.DependencyInjection;
4-
using OrderCloud.SDK;
5-
using NSubstitute;
6+
using Microsoft.Extensions.Hosting;
67
using Microsoft.OpenApi.Models;
78
using Newtonsoft.Json.Converters;
8-
using Microsoft.AspNetCore.Mvc;
9-
using Microsoft.Extensions.Hosting;
10-
using Microsoft.AspNetCore.Http;
9+
using NSubstitute;
10+
using OrderCloud.SDK;
11+
using OrderCloud.Catalyst;
12+
using System.Threading.Tasks;
1113

1214
namespace OrderCloud.Catalyst.TestApi
1315
{
@@ -45,6 +47,7 @@ public virtual void ConfigureServices(IServiceCollection services)
4547
builder => { builder.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader(); }));
4648
services
4749
.AddOrderCloudUserAuth(opts => opts.AddValidClientIDs(UnitTestClientID))
50+
.AddOrderCloudUserInfoAuth()
4851
.AddOrderCloudWebhookAuth(opts => opts.HashKey = _settings.OrderCloudSettings.WebhookHashKey)
4952
.AddSingleton<ISimpleCache, LazyCacheService>() // Replace LazyCacheService with RedisService if you have multiple server instances.
5053
.AddSingleton<IOrderCloudClient>(new OrderCloudClient(new OrderCloudClientConfig()
@@ -106,7 +109,16 @@ public override void ConfigureServices(IServiceCollection services)
106109
AuthUrl = "mockdomain.com",
107110
});
108111
oc.Me.GetAsync(Arg.Any<string>()).Returns(new MeUser { Username = "joe", Active = true, AvailableRoles = new[] { "Shopper" } });
109-
services.AddSingleton(oc);
112+
113+
114+
oc
115+
.GetPublicKeyAsync(Arg.Is<string>(k => k == TestRsaKeyProvider.AllowedKid))
116+
.Returns(Task.FromResult(TestRsaKeyProvider.ToOrderCloudPublicKey(TestRsaKeyProvider.AllowedRsa)));
117+
oc
118+
.GetPublicKeyAsync(Arg.Is<string>(k => k == TestRsaKeyProvider.DeniedKid))
119+
.Returns(Task.FromResult(TestRsaKeyProvider.ToOrderCloudPublicKey(TestRsaKeyProvider.DeniedRsa)));
120+
121+
services.AddSingleton(oc);
110122
}
111123

112124
public override void Configure(IApplicationBuilder app, IWebHostEnvironment env)
Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
using AutoFixture;
2+
using AutoFixture.NUnit3;
3+
using FluentAssertions;
4+
using Flurl.Http;
5+
using NSubstitute;
6+
using NUnit.Framework;
7+
using OrderCloud.Catalyst;
8+
using OrderCloud.Catalyst.TestApi;
9+
using OrderCloud.SDK;
10+
using System;
11+
using System.Collections.Generic;
12+
using System.Collections.Immutable;
13+
using System.Linq;
14+
using System.Net;
15+
using System.Text;
16+
using System.Threading.Tasks;
17+
18+
namespace OrderCloud.Catalyst.Tests
19+
{
20+
[TestFixture]
21+
public class UserInfoAuthTests
22+
{
23+
24+
[Test]
25+
public async Task should_deny_access_without_oc_token()
26+
{
27+
var resp = await TestFramework.Client
28+
.Request("demo/simpleuserinfo")
29+
.GetAsync();
30+
31+
await resp.ShouldHaveFirstApiError("InvalidToken", 401, "Access token is invalid or expired.");
32+
}
33+
34+
[Test]
35+
public async Task can_auth_with_oc_token()
36+
{
37+
var token = FakeUserInfoToken.Create();
38+
var result = await TestFramework.Client
39+
.WithOAuthBearerToken(token)
40+
.Request("demo/simpleuserinfo")
41+
.GetStringAsync();
42+
43+
result.Should().Be("\"hello userinfo!\"");
44+
}
45+
46+
[Test]
47+
public async Task should_succeed_with_custom_role()
48+
{
49+
var token = FakeUserInfoToken.Create(new List<string> { "CustomRole" });
50+
var request = TestFramework.Client
51+
.WithOAuthBearerToken(token)
52+
.Request("demo/customuserinfo");
53+
54+
var result = await request.GetStringAsync();
55+
56+
result.Should().Be("\"hello custom userinfo!\"");
57+
}
58+
59+
[Test]
60+
public async Task should_succeed_with_with_full_access()
61+
{
62+
var token = FakeUserInfoToken.Create(new List<string> { "FullAccess" });
63+
var request = TestFramework.Client
64+
.WithOAuthBearerToken(token)
65+
.Request("demo/customuserinfo");
66+
67+
var result = await request.GetStringAsync();
68+
69+
result.Should().Be("\"hello custom userinfo!\"");
70+
}
71+
72+
[Test]
73+
public async Task should_error_without_custom_role()
74+
{
75+
var token = FakeUserInfoToken.Create();
76+
var result = await TestFramework.Client
77+
.WithOAuthBearerToken(token)
78+
.Request("demo/customuserinfo")
79+
.GetAsync();
80+
81+
Assert.AreEqual(403, result.StatusCode);
82+
}
83+
84+
[Test]
85+
public async Task can_get_user_info_context_from_auth()
86+
{
87+
var fixture = new Fixture();
88+
var username = fixture.Create<string>();
89+
var token = FakeUserInfoToken.Create(new List<string> { "Shopper" }, username: username);
90+
91+
var result = await TestFramework.Client
92+
.WithOAuthBearerToken(token)
93+
.Request("demo/userinfocontext")
94+
.GetJsonAsync<SimplifiedUser>();
95+
96+
Assert.AreEqual(username, result.Username);
97+
Assert.AreEqual("Shopper", result.AvailableRoles[0]);
98+
}
99+
100+
[Test]
101+
public async Task can_get_user_context_from_setting_it()
102+
{
103+
var fixture = new Fixture();
104+
var username = fixture.Create<string>();
105+
var token = FakeUserInfoToken.Create(new List<string> { "Shopper" }, username: username);
106+
107+
var result = await TestFramework.Client
108+
.Request($"demo/userinfocontext/{token}")
109+
.PostAsync()
110+
.ReceiveJson<SimplifiedUser>();
111+
112+
Assert.AreEqual(username, result.Username);
113+
Assert.AreEqual("Shopper", result.AvailableRoles[0]);
114+
}
115+
116+
[Test]
117+
public async Task should_succeed_if_now_is_between_expiry_and_nvb()
118+
{
119+
var token = FakeUserInfoToken.Create(
120+
roles: new List<string> { "Shopper" },
121+
expiresUTC: DateTime.UtcNow + TimeSpan.FromHours(1),
122+
notValidBeforeUTC: DateTime.UtcNow - TimeSpan.FromHours(1)
123+
);
124+
125+
var resp = await TestFramework.Client
126+
.WithOAuthBearerToken(token)
127+
.Request("demo/simpleuserinfo")
128+
.GetStringAsync();
129+
130+
resp.Should().Be("\"hello userinfo!\"");
131+
}
132+
133+
[Test]
134+
public async Task should_deny_access_if_nvb_is_wrong()
135+
{
136+
var fixture = new Fixture();
137+
138+
var token = FakeUserInfoToken.Create(
139+
roles: new List<string> { "Shopper" },
140+
expiresUTC: DateTime.UtcNow + TimeSpan.FromHours(2),
141+
notValidBeforeUTC: DateTime.UtcNow + TimeSpan.FromHours(1)
142+
);
143+
144+
var resp = await TestFramework.Client
145+
.WithOAuthBearerToken(token)
146+
.Request("demo/simpleuserinfo")
147+
.GetAsync();
148+
149+
await resp.ShouldHaveFirstApiError("InvalidToken", 401, "Access token is invalid or expired.");
150+
}
151+
152+
[Test]
153+
public async Task should_deny_access_if_past_expiry()
154+
{
155+
var fixture = new Fixture();
156+
157+
var token = FakeUserInfoToken.Create(
158+
roles: new List<string> { "Shopper" },
159+
expiresUTC: DateTime.UtcNow,
160+
notValidBeforeUTC: DateTime.UtcNow - TimeSpan.FromHours(1)
161+
);
162+
163+
var resp = await TestFramework.Client
164+
.WithOAuthBearerToken(token)
165+
.Request("demo/simpleuserinfo")
166+
.GetAsync();
167+
168+
await resp.ShouldHaveFirstApiError("InvalidToken", 401, "Access token is invalid or expired.");
169+
}
170+
171+
[Test]
172+
public async Task two_requests_with_the_same_kid_should_verify_both_tokens()
173+
{
174+
var fixture = new Fixture();
175+
var keyID = fixture.Create<string>();
176+
177+
var token1 = FakeUserInfoToken.Create(new List<string> { "Shopper" }, keyID: keyID);
178+
var token2 = token1 + "makethisinvalid";
179+
180+
var response1 = await TestFramework.Client.WithOAuthBearerToken(token1).Request("demo/simpleuserinfo").GetAsync();
181+
var response2 = await TestFramework.Client.WithOAuthBearerToken(token2).Request("demo/simpleuserinfo").GetAsync();
182+
183+
await response2.ShouldHaveFirstApiError("InvalidToken", 401, "Access token is invalid or expired.");
184+
}
185+
186+
187+
188+
[Test]
189+
public async Task user_auth_provider_handles_mulitple_concurrent_requests()
190+
{
191+
var fixture = new Fixture();
192+
var requestCount = 10;
193+
var usernames = new List<string>();
194+
var requests = new List<Task<string>>();
195+
196+
foreach (var i in Enumerable.Range(0, requestCount))
197+
{
198+
var username = fixture.Create<string>();
199+
usernames.Add(username);
200+
var token = FakeUserInfoToken.Create(username: username);
201+
var request = TestFramework.Client.WithOAuthBearerToken(token).Request("demo/userinfousername").GetStringAsync();
202+
requests.Add(request);
203+
}
204+
205+
var results = await Task.WhenAll(requests);
206+
207+
foreach (var i in Enumerable.Range(0, requestCount))
208+
{
209+
Assert.AreEqual("\"" + usernames[i] + "\"", results[i]);
210+
}
211+
}
212+
}
213+
}

0 commit comments

Comments
 (0)