diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Impl/IdSite/DefaultIdSiteUrlBuilder_tests.cs b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/IdSite/DefaultIdSiteUrlBuilder_tests.cs
new file mode 100644
index 00000000..0411a8ab
--- /dev/null
+++ b/Stormpath.SDK/Stormpath.SDK.Tests/Impl/IdSite/DefaultIdSiteUrlBuilder_tests.cs
@@ -0,0 +1,202 @@
+//
+// Copyright (c) 2015 Stormpath, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+using System;
+using System.Collections.Generic;
+using NSubstitute;
+using Shouldly;
+using Stormpath.SDK.Api;
+using Stormpath.SDK.Client;
+using Stormpath.SDK.IdSite;
+using Stormpath.SDK.Impl.Client;
+using Stormpath.SDK.Impl.IdSite;
+using Stormpath.SDK.Impl.Utility;
+using Stormpath.SDK.Jwt;
+using Xunit;
+
+namespace Stormpath.SDK.Tests.Impl.IdSite
+{
+ public class DefaultIdSiteUrlBuilder_tests
+ {
+ private static readonly string FakeApiKeyId = "foobar";
+ private static readonly string FakeApiKeySecret = "veryLONGsupersecret_string123";
+
+ private static readonly string FakeApplicationHref = "https://api.stormpath.com/v1/applications/foobarApplication";
+ private static readonly string FakeCallbackUri = "http://my.app/login";
+
+ private static readonly string SsoUrl = "https://api.stormpath.com/sso";
+ private static readonly string JwtArg = "?jwtRequest=";
+
+ private readonly string fakeJti = "12345";
+ private readonly IIdSiteJtiProvider fakeJtiProvider;
+
+ private readonly DateTimeOffset fakeNow = new DateTimeOffset(2016, 01, 01, 00, 00, 00, TimeSpan.Zero);
+ private readonly IClock fakeClock;
+
+ private readonly IClient fakeClient;
+
+ public DefaultIdSiteUrlBuilder_tests()
+ {
+ this.fakeJtiProvider = Substitute.For();
+ this.fakeJtiProvider.NewJti().Returns(this.fakeJti);
+
+ this.fakeClock = Substitute.For();
+ this.fakeClock.Now.Returns(this.fakeNow);
+
+ var testApiKey = ClientApiKeys.Builder()
+ .SetId(FakeApiKeyId)
+ .SetSecret(FakeApiKeySecret)
+ .Build();
+
+ this.fakeClient = Clients.Builder()
+ .SetApiKey(testApiKey)
+ .Build();
+ }
+
+ private static readonly string PlainRequest = $"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIxMjM0NSIsImlhdCI6MTQ1MTYwNjQwMCwiaXNzIjoiZm9vYmFyIiwic3ViIjoiaHR0cHM6Ly9hcGkuc3Rvcm1wYXRoLmNvbS92MS9hcHBsaWNhdGlvbnMvZm9vYmFyQXBwbGljYXRpb24iLCJjYl91cmkiOiJodHRwOi8vbXkuYXBwL2xvZ2luIn0.s5tkKAJX3SzAey9fvaG_MGJCESpcLfWdM8PVtrwx5KI";
+
+ [Fact]
+ public void Generates_url()
+ {
+ var builder = this.GetBuilder();
+
+ var url = builder.Build();
+ url.ShouldBe($"{SsoUrl}{JwtArg}{PlainRequest}");
+
+ var jwt = this.ParseUrl(url);
+ this.VerifyCommon(jwt);
+ }
+
+ [Fact]
+ public void Generates_url_for_logout()
+ {
+ var builder = this.GetBuilder();
+ builder.ForLogout();
+
+ var url = builder.Build();
+ url.ShouldBe($"{SsoUrl}/logout{JwtArg}{PlainRequest}");
+ }
+
+ [Fact]
+ public void Generates_url_with_path()
+ {
+ var builder = this.GetBuilder();
+ builder.SetPath("/foo");
+
+ var url = builder.Build();
+
+ var jwt = this.ParseUrl(url);
+ this.VerifyCommon(jwt);
+ jwt.Body.GetClaim("path").ShouldBe("/foo");
+ }
+
+ [Fact]
+ public void Generates_url_with_organization_nameKey()
+ {
+ var builder = this.GetBuilder();
+ builder.SetOrganizationNameKey("gonk");
+
+ var url = builder.Build();
+
+ var jwt = this.ParseUrl(url);
+ this.VerifyCommon(jwt);
+ jwt.Body.GetClaim("onk").ShouldBe("gonk");
+ }
+
+ [Fact]
+ public void Generates_url_with_subdomain_flag()
+ {
+ var builder = this.GetBuilder();
+ builder.SetUseSubdomain(true);
+
+ var url = builder.Build();
+
+ var jwt = this.ParseUrl(url);
+ this.VerifyCommon(jwt);
+ jwt.Body.GetClaim("usd").ShouldBe(true);
+ }
+
+ [Fact]
+ public void Generates_url_with_organization_field_flag()
+ {
+ var builder = this.GetBuilder();
+ builder.SetShowOrganizationField(true);
+
+ var url = builder.Build();
+
+ var jwt = this.ParseUrl(url);
+ this.VerifyCommon(jwt);
+ jwt.Body.GetClaim("sof").ShouldBe(true);
+ }
+
+ [Fact]
+ public void Generates_url_with_state()
+ {
+ var builder = this.GetBuilder();
+ builder.SetState("foobar123!");
+
+ var url = builder.Build();
+
+ var jwt = this.ParseUrl(url);
+ this.VerifyCommon(jwt);
+ jwt.Body.GetClaim("state").ShouldBe("foobar123!");
+ }
+
+ [Fact]
+ public void Generates_url_with_all_the_things()
+ {
+ var builder = this.GetBuilder();
+ builder.ForLogout();
+ builder.SetOrganizationNameKey("FirstOrder");
+ builder.SetPath("/blah");
+ builder.SetShowOrganizationField(false);
+ builder.SetState("123");
+ builder.SetUseSubdomain(false);
+
+ var url = builder.Build();
+ url.ShouldStartWith($"{SsoUrl}/logout{JwtArg}");
+
+ var jwt = this.ParseUrl(url);
+ this.VerifyCommon(jwt);
+ jwt.Body.GetClaim("onk").ShouldBe("FirstOrder");
+ jwt.Body.GetClaim("path").ShouldBe("/blah");
+ jwt.Body.GetClaim("sof").ShouldBe(false);
+ jwt.Body.GetClaim("state").ShouldBe("123");
+ jwt.Body.GetClaim("usd").ShouldBe(false);
+ }
+
+ private IIdSiteUrlBuilder GetBuilder()
+ {
+ var client = this.fakeClient as DefaultClient;
+
+ IIdSiteUrlBuilder builder = new DefaultIdSiteUrlBuilder(
+ client.DataStore, FakeApplicationHref, this.fakeJtiProvider, this.fakeClock);
+ builder.SetCallbackUri(FakeCallbackUri);
+
+ return builder;
+ }
+
+ private IJwt ParseUrl(string url)
+ => this.fakeClient.NewJwtParser().Parse(url.Split('=')[1]);
+
+ private void VerifyCommon(IJwt jwt)
+ {
+ jwt.Body.Issuer.ShouldBe(FakeApiKeyId);
+ jwt.Body.Subject.ShouldBe(FakeApplicationHref);
+ jwt.Body.GetClaim("cb_uri").ShouldBe(FakeCallbackUri);
+ }
+ }
+}
diff --git a/Stormpath.SDK/Stormpath.SDK.Tests/Stormpath.SDK.Tests.csproj b/Stormpath.SDK/Stormpath.SDK.Tests/Stormpath.SDK.Tests.csproj
index 48f0be02..cfe38cf0 100644
--- a/Stormpath.SDK/Stormpath.SDK.Tests/Stormpath.SDK.Tests.csproj
+++ b/Stormpath.SDK/Stormpath.SDK.Tests/Stormpath.SDK.Tests.csproj
@@ -122,6 +122,7 @@
+
diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Application/DefaultApplication.IdSite.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Application/DefaultApplication.IdSite.cs
index d36fd332..a477255f 100644
--- a/Stormpath.SDK/Stormpath.SDK/Impl/Application/DefaultApplication.IdSite.cs
+++ b/Stormpath.SDK/Stormpath.SDK/Impl/Application/DefaultApplication.IdSite.cs
@@ -16,13 +16,14 @@
using Stormpath.SDK.Application;
using Stormpath.SDK.Impl.IdSite;
+using Stormpath.SDK.Impl.Utility;
namespace Stormpath.SDK.Impl.Application
{
internal sealed partial class DefaultApplication
{
SDK.IdSite.IIdSiteUrlBuilder IApplication.NewIdSiteUrlBuilder()
- => new DefaultIdSiteUrlBuilder(this.GetInternalDataStore(), this.AsInterface.Href);
+ => new DefaultIdSiteUrlBuilder(this.GetInternalDataStore(), this.AsInterface.Href, new DefaultIdSiteJtiProvider(), new DefaultClock());
SDK.IdSite.IIdSiteAsyncCallbackHandler IApplication.NewIdSiteAsyncCallbackHandler(SDK.Http.IHttpRequest request)
=> new DefaultIdSiteAsyncCallbackHandler(this.GetInternalDataStore(), request);
diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/IdSite/DefaultIdSiteJtiProvider.cs b/Stormpath.SDK/Stormpath.SDK/Impl/IdSite/DefaultIdSiteJtiProvider.cs
new file mode 100644
index 00000000..c70d8399
--- /dev/null
+++ b/Stormpath.SDK/Stormpath.SDK/Impl/IdSite/DefaultIdSiteJtiProvider.cs
@@ -0,0 +1,25 @@
+//
+// Copyright (c) 2015 Stormpath, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+using System;
+
+namespace Stormpath.SDK.Impl.IdSite
+{
+ internal sealed class DefaultIdSiteJtiProvider : IIdSiteJtiProvider
+ {
+ string IIdSiteJtiProvider.NewJti() => Guid.NewGuid().ToString();
+ }
+}
diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/IdSite/DefaultIdSiteUrlBuilder.cs b/Stormpath.SDK/Stormpath.SDK/Impl/IdSite/DefaultIdSiteUrlBuilder.cs
index 407e528f..49d77996 100644
--- a/Stormpath.SDK/Stormpath.SDK/Impl/IdSite/DefaultIdSiteUrlBuilder.cs
+++ b/Stormpath.SDK/Stormpath.SDK/Impl/IdSite/DefaultIdSiteUrlBuilder.cs
@@ -19,6 +19,7 @@
using Stormpath.SDK.IdSite;
using Stormpath.SDK.Impl.DataStore;
using Stormpath.SDK.Impl.Jwt;
+using Stormpath.SDK.Impl.Utility;
using Stormpath.SDK.Jwt;
namespace Stormpath.SDK.Impl.IdSite
@@ -31,6 +32,9 @@ internal sealed class DefaultIdSiteUrlBuilder : IIdSiteUrlBuilder
private readonly string ssoEndpoint;
private readonly string applicationHref;
+ private readonly IIdSiteJtiProvider jtiProvider;
+ private readonly IClock clock;
+
private string callbackUri;
private string state;
private string path;
@@ -39,7 +43,7 @@ internal sealed class DefaultIdSiteUrlBuilder : IIdSiteUrlBuilder
private bool? useSubdomain;
private bool? showOrganizationField;
- public DefaultIdSiteUrlBuilder(IInternalDataStore internalDataStore, string applicationHref)
+ public DefaultIdSiteUrlBuilder(IInternalDataStore internalDataStore, string applicationHref, IIdSiteJtiProvider jtiProvider, IClock clock)
{
if (internalDataStore == null)
{
@@ -51,7 +55,19 @@ public DefaultIdSiteUrlBuilder(IInternalDataStore internalDataStore, string appl
throw new ArgumentNullException(nameof(applicationHref));
}
+ if (jtiProvider == null)
+ {
+ throw new ArgumentNullException(nameof(jtiProvider));
+ }
+
+ if (clock == null)
+ {
+ throw new ArgumentNullException(nameof(clock));
+ }
+
this.internalDataStore = internalDataStore;
+ this.jtiProvider = jtiProvider;
+ this.clock = clock;
this.applicationHref = applicationHref;
this.ssoEndpoint = GetBaseUrl(applicationHref) + "/sso";
}
@@ -144,17 +160,17 @@ string IIdSiteUrlBuilder.Build()
throw new ApplicationException($"{nameof(this.callbackUri)} cannot be null or empty.");
}
- var jti = Guid.NewGuid().ToString();
- var now = DateTimeOffset.UtcNow;
+ var jti = this.jtiProvider.NewJti();
var apiKey = this.internalDataStore.ApiKey;
IJwtBuilder jwtBuilder = new DefaultJwtBuilder(this.internalDataStore.Serializer);
jwtBuilder
.SetId(jti)
- .SetIssuedAt(DateTimeOffset.Now)
+ .SetIssuedAt(this.clock.Now)
.SetIssuer(apiKey.GetId())
.SetSubject(this.applicationHref)
- .SetClaim(IdSiteClaims.RedirectUri, this.callbackUri);
+ .SetClaim(IdSiteClaims.RedirectUri, this.callbackUri)
+ .SignWith(apiKey.GetSecret(), Encoding.UTF8);
if (!string.IsNullOrEmpty(this.path))
{
@@ -171,12 +187,12 @@ string IIdSiteUrlBuilder.Build()
jwtBuilder.SetClaim(IdSiteClaims.OrganizationNameKey, this.organizationNameKey);
}
- if (this.useSubdomain.HasValue)
+ if (this.useSubdomain != null)
{
jwtBuilder.SetClaim(IdSiteClaims.UseSubdomain, this.useSubdomain.Value);
}
- if (this.showOrganizationField.HasValue)
+ if (this.showOrganizationField != null)
{
jwtBuilder.SetClaim(IdSiteClaims.ShowOrganizationField, this.showOrganizationField.Value);
}
diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/IdSite/IIdSiteJtiProvider.cs b/Stormpath.SDK/Stormpath.SDK/Impl/IdSite/IIdSiteJtiProvider.cs
new file mode 100644
index 00000000..56a6646a
--- /dev/null
+++ b/Stormpath.SDK/Stormpath.SDK/Impl/IdSite/IIdSiteJtiProvider.cs
@@ -0,0 +1,33 @@
+//
+// Copyright (c) 2015 Stormpath, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+namespace Stormpath.SDK.Impl.IdSite
+{
+ ///
+ /// Represents a JWT ID (jti) provider used for ID Site.
+ ///
+ ///
+ /// This is abstracted so it can be injectable during testing.
+ ///
+ internal interface IIdSiteJtiProvider
+ {
+ ///
+ /// Generates a new JWT ID (jti) string.
+ ///
+ /// A JWT ID string.
+ string NewJti();
+ }
+}
diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Utility/DefaultClock.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Utility/DefaultClock.cs
new file mode 100644
index 00000000..b8287798
--- /dev/null
+++ b/Stormpath.SDK/Stormpath.SDK/Impl/Utility/DefaultClock.cs
@@ -0,0 +1,25 @@
+//
+// Copyright (c) 2015 Stormpath, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+using System;
+
+namespace Stormpath.SDK.Impl.Utility
+{
+ internal sealed class DefaultClock : IClock
+ {
+ DateTimeOffset IClock.Now => DateTimeOffset.Now;
+ }
+}
diff --git a/Stormpath.SDK/Stormpath.SDK/Impl/Utility/IClock.cs b/Stormpath.SDK/Stormpath.SDK/Impl/Utility/IClock.cs
new file mode 100644
index 00000000..d4929ab4
--- /dev/null
+++ b/Stormpath.SDK/Stormpath.SDK/Impl/Utility/IClock.cs
@@ -0,0 +1,37 @@
+//
+// Copyright (c) 2015 Stormpath, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+using System;
+
+namespace Stormpath.SDK.Impl.Utility
+{
+ ///
+ /// Injectable clock interface for testing.
+ ///
+ internal interface IClock
+ {
+ ///
+ /// Gets a object that is set to the current date and time
+ /// on the current computer, with the offset set to the local time's offset from
+ /// Coordinated Universal Time (UTC).
+ ///
+ ///
+ /// A System.DateTimeOffset object whose date and time is the current local time
+ /// and whose offset is the local time zone's offset from Coordinated Universal Time (UTC).
+ ///
+ DateTimeOffset Now { get; }
+ }
+}
diff --git a/Stormpath.SDK/Stormpath.SDK/Stormpath.SDK.csproj b/Stormpath.SDK/Stormpath.SDK/Stormpath.SDK.csproj
index d47451ea..62366da4 100644
--- a/Stormpath.SDK/Stormpath.SDK/Stormpath.SDK.csproj
+++ b/Stormpath.SDK/Stormpath.SDK/Stormpath.SDK.csproj
@@ -122,6 +122,7 @@
+
@@ -264,6 +265,7 @@
+
@@ -453,9 +455,11 @@
+
+