diff --git a/docs/design-principles/0110-back-end-for-front-end.md b/docs/design-principles/0110-back-end-for-front-end.md
index b1bcf708..4d57c9a1 100644
--- a/docs/design-principles/0110-back-end-for-front-end.md
+++ b/docs/design-principles/0110-back-end-for-front-end.md
@@ -81,8 +81,6 @@ If the attempt to authenticate is successful, the authentication response from t
These "auth tokens" are then added to [HTTPOnly, secure] cookies that are then returned to the browser to be used between the browser and BEFFE for subsequent requests/responses.
-![Authentication](../images/Authentication-Credentials.png)
-
At some point in time, either of those auth tokens will expire, at which point either the `access_token` can be refreshed (using the `refresh_token`), or the `refresh_tokesn` expires, and the end user will need to re-authenticate again.
#### Login
@@ -99,20 +97,26 @@ For example,
}
```
+![Credentials Authentication](../images/Authentication-Credentials.png)
+
or with a body containing an SSO authentication code,
For example,
```json
{
- "AuthCode": "anauthocde",
- "Provider": "sso"
+ "AuthCode": "anauthcode",
+ "Provider": "google"
}
```
+> Note: The Backend API call to authenticate this "OAuth2 identity" will receive some tokens (from the 3rd party provider) that should include an email address \[claim\] that identifies the actual person to the system. However, some OAuth2 providers today do not include that email address claim in the returned tokens, and in those cases the parson cannot be identified. Instead, their email address can be included in the above request (as `Username`) which can be available in the first steps of the "OAuth Authorization Flow".
+
+![SSO Authentication](../images/Authentication-SSO.png)
+
> Note: you will also need to include CSRF protection in these requests, like all others coming from a JS app.
-A successful response from this request will yield the following body,
+A successful response from either of these requests will yield the following body,
For example,
@@ -122,7 +126,7 @@ For example,
}
```
-But it will also include these cookies (for the current domain):
+But, the response will also include these cookies (for the current domain):
`auth-tok=anaccesstoken`
diff --git a/docs/images/Authentication-SSO.png b/docs/images/Authentication-SSO.png
index c153e375..82be792d 100644
Binary files a/docs/images/Authentication-SSO.png and b/docs/images/Authentication-SSO.png differ
diff --git a/docs/images/BEFFE-ReverseProxy.png b/docs/images/BEFFE-ReverseProxy.png
index 844a0acc..e76b4c8d 100644
Binary files a/docs/images/BEFFE-ReverseProxy.png and b/docs/images/BEFFE-ReverseProxy.png differ
diff --git a/docs/images/Sources.pptx b/docs/images/Sources.pptx
index 9e7a652d..4728b56b 100644
Binary files a/docs/images/Sources.pptx and b/docs/images/Sources.pptx differ
diff --git a/src/ApiHost1/appsettings.json b/src/ApiHost1/appsettings.json
index aface782..b9cdf03f 100644
--- a/src/ApiHost1/appsettings.json
+++ b/src/ApiHost1/appsettings.json
@@ -23,6 +23,11 @@
"SenderProductName": "SaaStack",
"SenderEmailAddress": "noreply@saastack.com",
"SenderDisplayName": "Support"
+ },
+ "SSOProvidersService": {
+ "SSOUserTokens": {
+ "AesSecret": "V7z5SZnhHRa7z68adsvazQjeIbSiWWcR+4KuAUikhe0=::u4ErEVotb170bM8qKWyT8A=="
+ }
}
},
"Hosts": {
diff --git a/src/Application.Interfaces/Audits.Designer.cs b/src/Application.Interfaces/Audits.Designer.cs
index 225054d9..dbfd696d 100644
--- a/src/Application.Interfaces/Audits.Designer.cs
+++ b/src/Application.Interfaces/Audits.Designer.cs
@@ -130,5 +130,32 @@ public static string PasswordCredentialsApplication_Authenticate_Succeeded {
return ResourceManager.GetString("PasswordCredentialsApplication_Authenticate_Succeeded", resourceCulture);
}
}
+
+ ///
+ /// Looks up a localized string similar to SingleSignOn.AutoRegistered.
+ ///
+ public static string SingleSignOnApplication_Authenticate_AccountOnboarded {
+ get {
+ return ResourceManager.GetString("SingleSignOnApplication_Authenticate_AccountOnboarded", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Authentication.Failed.AccountSuspended.
+ ///
+ public static string SingleSignOnApplication_Authenticate_AccountSuspended {
+ get {
+ return ResourceManager.GetString("SingleSignOnApplication_Authenticate_AccountSuspended", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Authentication.Passed.
+ ///
+ public static string SingleSignOnApplication_Authenticate_Succeeded {
+ get {
+ return ResourceManager.GetString("SingleSignOnApplication_Authenticate_Succeeded", resourceCulture);
+ }
+ }
}
}
diff --git a/src/Application.Interfaces/Audits.resx b/src/Application.Interfaces/Audits.resx
index 9ea57fe6..e7167c26 100644
--- a/src/Application.Interfaces/Audits.resx
+++ b/src/Application.Interfaces/Audits.resx
@@ -48,4 +48,13 @@
EndUser.PlatformRolesAssigned
+
+ SingleSignOn.AutoRegistered
+
+
+ Authentication.Failed.AccountSuspended
+
+
+ Authentication.Passed
+
\ No newline at end of file
diff --git a/src/Application.Resources.Shared/Identity.cs b/src/Application.Resources.Shared/Identity.cs
index 0d64b32d..627f3ea5 100644
--- a/src/Application.Resources.Shared/Identity.cs
+++ b/src/Application.Resources.Shared/Identity.cs
@@ -4,15 +4,18 @@ namespace Application.Resources.Shared;
public class AuthenticateTokens
{
- public required string AccessToken { get; set; }
+ public required AuthenticateToken AccessToken { get; set; }
- public required DateTime AccessTokenExpiresOn { get; set; }
+ public required AuthenticateToken RefreshToken { get; set; }
- public required string RefreshToken { get; set; }
+ public required string UserId { get; set; }
+}
- public required DateTime RefreshTokenExpiresOn { get; set; }
+public class AuthenticateToken
+{
+ public required DateTime ExpiresOn { get; set; }
- public required string UserId { get; set; }
+ public required string Value { get; set; }
}
public class APIKey : IIdentifiableResource
@@ -26,4 +29,27 @@ public class APIKey : IIdentifiableResource
public required string UserId { get; set; }
public required string Id { get; set; }
+}
+
+public class AuthToken
+{
+ public AuthToken(TokenType type, string value, DateTime? expiresOn)
+ {
+ Type = type;
+ Value = value;
+ ExpiresOn = expiresOn;
+ }
+
+ public DateTime? ExpiresOn { get; }
+
+ public TokenType Type { get; }
+
+ public string Value { get; }
+}
+
+public enum TokenType
+{
+ AccessToken = 1,
+ RefreshToken = 2,
+ IdToken = 3
}
\ No newline at end of file
diff --git a/src/Application.Services.Shared/IEndUsersService.cs b/src/Application.Services.Shared/IEndUsersService.cs
index 3c3a4853..86ec2275 100644
--- a/src/Application.Services.Shared/IEndUsersService.cs
+++ b/src/Application.Services.Shared/IEndUsersService.cs
@@ -6,15 +6,18 @@ namespace Application.Services.Shared;
public interface IEndUsersService
{
- Task> GetPersonAsync(ICallerContext caller, string id, CancellationToken cancellationToken);
+ Task, Error>> FindPersonByEmailAsync(ICallerContext caller, string emailAddress,
+ CancellationToken cancellationToken);
- Task> GetMembershipsAsync(ICallerContext context, string id,
+ Task> GetMembershipsAsync(ICallerContext caller, string id,
CancellationToken cancellationToken);
- Task> RegisterMachineAsync(ICallerContext context, string name, string? timezone,
+ Task> GetPersonAsync(ICallerContext caller, string id, CancellationToken cancellationToken);
+
+ Task> RegisterMachineAsync(ICallerContext caller, string name, string? timezone,
string? countryCode, CancellationToken cancellationToken);
Task> RegisterPersonAsync(ICallerContext caller, string emailAddress,
- string firstName, string lastName, string? timezone, string? countryCode, bool termsAndConditionsAccepted,
+ string firstName, string? lastName, string? timezone, string? countryCode, bool termsAndConditionsAccepted,
CancellationToken cancellationToken);
}
\ No newline at end of file
diff --git a/src/Common/Extensions/EnumExtensions.cs b/src/Common/Extensions/EnumExtensions.cs
index f0cb892c..45fab797 100644
--- a/src/Common/Extensions/EnumExtensions.cs
+++ b/src/Common/Extensions/EnumExtensions.cs
@@ -24,7 +24,7 @@ public static TTargetEnum ToEnum(this TSourceEnum sour
/// Converts the to an value of the ,
/// and in the case where no value can be found, uses the
///
- public static TTargetEnum ToEnumOrDefault(this string value, TTargetEnum defaultValue)
+ public static TTargetEnum ToEnumOrDefault(this string? value, TTargetEnum defaultValue)
{
if (value.HasNoValue())
{
diff --git a/src/Domain.Interfaces/Domain.Interfaces.csproj b/src/Domain.Interfaces/Domain.Interfaces.csproj
index 5be2f664..7379efbe 100644
--- a/src/Domain.Interfaces/Domain.Interfaces.csproj
+++ b/src/Domain.Interfaces/Domain.Interfaces.csproj
@@ -11,6 +11,7 @@
+
diff --git a/src/Domain.Interfaces/Validations/CommonValidations.cs b/src/Domain.Interfaces/Validations/CommonValidations.cs
index 23f1dc15..039dfd1d 100644
--- a/src/Domain.Interfaces/Validations/CommonValidations.cs
+++ b/src/Domain.Interfaces/Validations/CommonValidations.cs
@@ -1,5 +1,6 @@
using Common;
using Common.Extensions;
+using PhoneNumbers;
namespace Domain.Interfaces.Validations;
@@ -14,6 +15,34 @@ public static class CommonValidations
public static readonly Validation FeatureLevel = new(@"^[\w\d]{4,30}$", 4, 30);
public static readonly Validation Identifier = new(@"^[\w]{1,20}_[\d\w]{10,22}$", 12, 43);
public static readonly Validation IdentifierPrefix = new(@"^[^\W_]*$", 1, 20);
+
+ ///
+ /// Validations for International
+ ///
+ public static readonly Validation PhoneNumber = new(value =>
+ {
+ if (!value.HasValue())
+ {
+ return false;
+ }
+
+ if (!value.StartsWith("+"))
+ {
+ return false;
+ }
+
+ var util = PhoneNumberUtil.GetInstance();
+ try
+ {
+ var number = util.Parse(value, null);
+ return util.IsValidNumber(number);
+ }
+ catch (NumberParseException)
+ {
+ return false;
+ }
+ });
+
public static readonly Validation RoleLevel = new(@"^[\w\d]{4,30}$", 4, 30);
public static readonly Validation Timezone = new(Timezones.Exists);
public static readonly Validation Url = new(s => Uri.IsWellFormedUriString(s, UriKind.Absolute));
diff --git a/src/Domain.Shared/Address.cs b/src/Domain.Shared/Address.cs
new file mode 100644
index 00000000..0d914579
--- /dev/null
+++ b/src/Domain.Shared/Address.cs
@@ -0,0 +1,68 @@
+using Common;
+using Domain.Common.ValueObjects;
+using Domain.Interfaces;
+
+namespace Domain.Shared;
+
+public sealed class Address : ValueObjectBase
+{
+ public static readonly Address Default = Create(CountryCodes.Default).Value;
+
+ public static Result Create(string line1, string line2, string line3, string city, string state,
+ CountryCodeIso3166 countryCode, string zip)
+ {
+ return new Address(line1, line2, line3, city, state, countryCode, zip);
+ }
+
+ public static Result Create(CountryCodeIso3166 countryCode)
+ {
+ return new Address(countryCode);
+ }
+
+ private Address(CountryCodeIso3166 countryCode) : this(string.Empty, string.Empty, string.Empty,
+ string.Empty, string.Empty, countryCode, string.Empty)
+ {
+ }
+
+ private Address(string? line1, string? line2, string? line3, string? city, string? state,
+ CountryCodeIso3166 countryCode, string? zip)
+ {
+ Line1 = line1;
+ Line2 = line2;
+ Line3 = line3;
+ City = city;
+ State = state;
+ CountryCode = countryCode;
+ Zip = zip;
+ }
+
+ public string? City { get; }
+
+ public CountryCodeIso3166 CountryCode { get; }
+
+ public string? Line1 { get; }
+
+ public string? Line2 { get; }
+
+ public string? Line3 { get; }
+
+ public string? State { get; }
+
+ public string? Zip { get; }
+
+ public static ValueObjectFactory Rehydrate()
+ {
+ return (property, _) =>
+ {
+ var parts = RehydrateToList(property, false);
+ return new Address(parts[0], parts[1], parts[2], parts[3], parts[4],
+ CountryCodes.FindOrDefault(parts[5]),
+ parts[6]);
+ };
+ }
+
+ protected override IEnumerable