Skip to content

Commit

Permalink
Added SSO authentication with a FakeSSOAuthenticationProvider to prov…
Browse files Browse the repository at this point in the history
…ide an example. #4
  • Loading branch information
jezzsantos committed Feb 12, 2024
1 parent 81e6e3a commit 5266d4a
Show file tree
Hide file tree
Showing 86 changed files with 2,704 additions and 265 deletions.
16 changes: 10 additions & 6 deletions docs/design-principles/0110-back-end-for-front-end.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,

Expand All @@ -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`

Expand Down
Binary file modified docs/images/Authentication-SSO.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/BEFFE-ReverseProxy.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/Sources.pptx
Binary file not shown.
5 changes: 5 additions & 0 deletions src/ApiHost1/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@
"SenderProductName": "SaaStack",
"SenderEmailAddress": "[email protected]",
"SenderDisplayName": "Support"
},
"SSOProvidersService": {
"SSOUserTokens": {
"AesSecret": "V7z5SZnhHRa7z68adsvazQjeIbSiWWcR+4KuAUikhe0=::u4ErEVotb170bM8qKWyT8A=="
}
}
},
"Hosts": {
Expand Down
27 changes: 27 additions & 0 deletions src/Application.Interfaces/Audits.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions src/Application.Interfaces/Audits.resx
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,13 @@
<data name="EndUserApplication_PlatformRolesAssigned" xml:space="preserve">
<value>EndUser.PlatformRolesAssigned</value>
</data>
<data name="SingleSignOnApplication_Authenticate_AccountOnboarded" xml:space="preserve">
<value>SingleSignOn.AutoRegistered</value>
</data>
<data name="SingleSignOnApplication_Authenticate_AccountSuspended" xml:space="preserve">
<value>Authentication.Failed.AccountSuspended</value>
</data>
<data name="SingleSignOnApplication_Authenticate_Succeeded" xml:space="preserve">
<value>Authentication.Passed</value>
</data>
</root>
36 changes: 31 additions & 5 deletions src/Application.Resources.Shared/Identity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
}
11 changes: 7 additions & 4 deletions src/Application.Services.Shared/IEndUsersService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,18 @@ namespace Application.Services.Shared;

public interface IEndUsersService
{
Task<Result<EndUser, Error>> GetPersonAsync(ICallerContext caller, string id, CancellationToken cancellationToken);
Task<Result<Optional<EndUser>, Error>> FindPersonByEmailAsync(ICallerContext caller, string emailAddress,
CancellationToken cancellationToken);

Task<Result<EndUserWithMemberships, Error>> GetMembershipsAsync(ICallerContext context, string id,
Task<Result<EndUserWithMemberships, Error>> GetMembershipsAsync(ICallerContext caller, string id,
CancellationToken cancellationToken);

Task<Result<RegisteredEndUser, Error>> RegisterMachineAsync(ICallerContext context, string name, string? timezone,
Task<Result<EndUser, Error>> GetPersonAsync(ICallerContext caller, string id, CancellationToken cancellationToken);

Task<Result<RegisteredEndUser, Error>> RegisterMachineAsync(ICallerContext caller, string name, string? timezone,
string? countryCode, CancellationToken cancellationToken);

Task<Result<RegisteredEndUser, Error>> 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);
}
2 changes: 1 addition & 1 deletion src/Common/Extensions/EnumExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public static TTargetEnum ToEnum<TSourceEnum, TTargetEnum>(this TSourceEnum sour
/// Converts the <see cref="value" /> to an value of the <see cref="TTargetEnum" />,
/// and in the case where no value can be found, uses the <see cref="defaultValue" />
/// </summary>
public static TTargetEnum ToEnumOrDefault<TTargetEnum>(this string value, TTargetEnum defaultValue)
public static TTargetEnum ToEnumOrDefault<TTargetEnum>(this string? value, TTargetEnum defaultValue)
{
if (value.HasNoValue())
{
Expand Down
1 change: 1 addition & 0 deletions src/Domain.Interfaces/Domain.Interfaces.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

<ItemGroup>
<PackageReference Include="QueryAny" Version="1.1.1" />
<PackageReference Include="libphonenumber-csharp" Version="8.12.52" />
</ItemGroup>

<ItemGroup>
Expand Down
29 changes: 29 additions & 0 deletions src/Domain.Interfaces/Validations/CommonValidations.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Common;
using Common.Extensions;
using PhoneNumbers;

namespace Domain.Interfaces.Validations;

Expand All @@ -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);

/// <summary>
/// Validations for International
/// </summary>
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));
Expand Down
68 changes: 68 additions & 0 deletions src/Domain.Shared/Address.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
using Common;
using Domain.Common.ValueObjects;
using Domain.Interfaces;

namespace Domain.Shared;

public sealed class Address : ValueObjectBase<Address>
{
public static readonly Address Default = Create(CountryCodes.Default).Value;

public static Result<Address, Error> 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<Address, Error> 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<Address> 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<object?> GetAtomicValues()
{
return new object?[] { Line1, Line2, Line3, City, State, CountryCode.Alpha3, Zip };
}
}
38 changes: 38 additions & 0 deletions src/Domain.Shared/PhoneNumber.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using Common;
using Common.Extensions;
using Domain.Common.Extensions;
using Domain.Common.ValueObjects;
using Domain.Interfaces;
using Domain.Interfaces.Validations;

namespace Domain.Shared;

public sealed class PhoneNumber : SingleValueObjectBase<PhoneNumber, string>
{
public static Result<PhoneNumber, Error> Create(string phoneNumber)
{
if (phoneNumber.IsNotValuedParameter(nameof(phoneNumber), out var error1))
{
return error1;
}

if (phoneNumber.IsInvalidParameter(CommonValidations.PhoneNumber, nameof(phoneNumber),
Resources.PhoneNumber_InvalidPhoneNumber, out var error2))
{
return error2;
}

return new PhoneNumber(phoneNumber);
}

private PhoneNumber(string phoneNumber) : base(phoneNumber)
{
}

public string Number => Value;

public static ValueObjectFactory<PhoneNumber> Rehydrate()
{
return (property, _) => new PhoneNumber(property);
}
}
18 changes: 18 additions & 0 deletions src/Domain.Shared/Resources.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions src/Domain.Shared/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,10 @@
<data name="Features_InvalidFeature" xml:space="preserve">
<value>The Feature value is invalid</value>
</data>
<data name="Timezone_InvalidTimezone" xml:space="preserve">
<value>The Timezone value is invalid</value>
</data>
<data name="PhoneNumber_InvalidPhoneNumber" xml:space="preserve">
<value>The PhoneNumber value is not a valid international phone number</value>
</data>
</root>
Loading

0 comments on commit 5266d4a

Please sign in to comment.