Skip to content

Commit

Permalink
Merge pull request #4312 from signalco-io/feat/cloud-pat
Browse files Browse the repository at this point in the history
feat(cloud): Added PAT for signalco endpoints
  • Loading branch information
AleksandarDev authored Jan 12, 2024
2 parents 69f8500 + 4bc0494 commit fb309f0
Show file tree
Hide file tree
Showing 24 changed files with 359 additions and 41 deletions.
46 changes: 26 additions & 20 deletions cloud/src/Signal.Api.Common/Auth/Auth0Authenticator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Security.Claims;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.IdentityModel.Protocols;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using Microsoft.IdentityModel.Tokens;
using Signal.Core.Secrets;

namespace Signal.Api.Common.Auth;

Expand All @@ -15,18 +17,14 @@ namespace Signal.Api.Common.Auth;
/// </summary>
public sealed class Auth0Authenticator : IJwtAuthenticator
{
private readonly ISecretsProvider secretsProvider;
private readonly TokenValidationParameters parameters;
private readonly ConfigurationManager<OpenIdConnectConfiguration> manager;
private readonly JwtSecurityTokenHandler handler;

/// <summary>
/// Creates a new authenticator. In most cases, you should only have one authenticator instance in your application.
/// </summary>
/// <param name="auth0Domain">The domain of the Auth0 account, e.g., <c>"myauth0test.auth0.com"</c>.</param>
/// <param name="audiences">The valid audiences for tokens. This must include the "audience" of the access_token request, and may also include a "client id" to enable id_tokens from clients you own.</param>
/// <param name="allowExpired">Set to <c>True</c> to disable token lifetime validation; should be almost always <c>False</c>. In case of validating token when refreshing access token - set to <c>true</c>.</param>
public Auth0Authenticator(string auth0Domain, IEnumerable<string> audiences, bool allowExpired)
public Auth0Authenticator(string auth0Domain, IEnumerable<string> audiences, bool allowExpired, ISecretsProvider secretsProvider)
{
this.secretsProvider = secretsProvider;
this.manager = new ConfigurationManager<OpenIdConnectConfiguration>(
$"https://{auth0Domain}/.well-known/openid-configuration",
new OpenIdConnectConfigurationRetriever());
Expand All @@ -40,22 +38,30 @@ public Auth0Authenticator(string auth0Domain, IEnumerable<string> audiences, boo
this.handler = new JwtSecurityTokenHandler();
}

/// <summary>
/// Authenticates the user token. Returns a user principal containing claims from the token and a token that can be used to perform actions on behalf of the user.
/// Throws an exception if the token fails to authenticate.
/// This method has an asynchronous signature, but usually completes synchronously.
/// </summary>
/// <param name="token">The token, in JWT format.</param>
/// <param name="cancellationToken">An optional cancellation token.</param>
public async Task<(ClaimsPrincipal User, SecurityToken ValidatedToken)> AuthenticateAsync(
string token,
CancellationToken cancellationToken = default)
{
// Note: ConfigurationManager<T> has an automatic refresh interval of 1 day.
// The config is cached in-between refreshes, so this "asynchronous" call actually completes synchronously unless it needs to refresh.
var config = await this.manager.GetConfigurationAsync(cancellationToken);
this.parameters.IssuerSigningKeys = config.SigningKeys;
var user = this.handler.ValidateToken(token, this.parameters, out var validatedToken);
return (user, validatedToken);
if (this.handler.ReadJwtToken(token).Issuer == "signalcopat") // Same as in PatService (where PAT is created)
{
// TODO: Optimize by caching these parameters (not changing)
var patParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(
await this.secretsProvider.GetSecretAsync(SecretKeys.PatSigningToken, cancellationToken)))
};
var user = this.handler.ValidateToken(token, patParameters, out var validatedToken);
return (user, validatedToken);
}
else
{
// Note: ConfigurationManager<T> has an automatic refresh interval of 1 day.
// The config is cached in-between refreshes, so this "asynchronous" call actually completes synchronously unless it needs to refresh.
var auth0Config = await this.manager.GetConfigurationAsync(cancellationToken);
this.parameters.IssuerSigningKeys = auth0Config.SigningKeys;
var user = this.handler.ValidateToken(token, this.parameters, out var validatedToken);
return (user, validatedToken);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ private async Task<Auth0Authenticator> InitializeAuthenticatorAsync(bool allowEx
{
var domain = await secretsProvider.GetSecretAsync(SecretKeys.Auth0.Domain, cancellationToken);
var audience = await secretsProvider.GetSecretAsync(SecretKeys.Auth0.ApiIdentifier, cancellationToken);
return new Auth0Authenticator(domain, new[] {audience}, allowExpiredToken);
return new Auth0Authenticator(domain, new[] {audience}, allowExpiredToken, secretsProvider);
}

public async Task<IUserRefreshToken> RefreshTokenAsync(
Expand Down
20 changes: 20 additions & 0 deletions cloud/src/Signal.Api.Common/Auth/PatDto.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using System;
using System.Text.Json.Serialization;

namespace Signal.Api.Common.Auth;

[Serializable]
public sealed class PatDto(string userId, string patEnd, string? alias, DateTime? expire)
{
[JsonPropertyName("userId")]
public string UserId { get; } = userId;

[JsonPropertyName("patEnd")]
public string PatEnd { get; } = patEnd;

[JsonPropertyName("alias")]
public string? Alias { get; } = alias;

[JsonPropertyName("expire")]
public DateTime? Expire { get; } = expire;
}
2 changes: 1 addition & 1 deletion cloud/src/Signal.Api.Common/Auth/UserAuth.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,4 @@ public UserAuth(string userId)
}

public string UserId { get; }
}
}
25 changes: 25 additions & 0 deletions cloud/src/Signal.Core/Auth/IPat.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using System;

namespace Signal.Core.Auth;

public class Pat : IPat
{
public required string UserId { get; set; }
public required string PatEnd { get; set; }
public required string PatHash { get; set; }
public string? Alias { get; set; }
public DateTime? Expire { get; set; }
}

public interface IPat
{
string UserId { get; set; }

string PatEnd { get; set; }

string PatHash { get; set; }

string? Alias { get; set; }

DateTime? Expire { get; set; }
}
12 changes: 12 additions & 0 deletions cloud/src/Signal.Core/Auth/IPatCreate.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using System;

namespace Signal.Core.Auth;

public interface IPatCreate
{
string UserId { get; }

string? Alias { get; }

DateTime? Expire { get; }
}
14 changes: 14 additions & 0 deletions cloud/src/Signal.Core/Auth/IPatService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

namespace Signal.Core.Auth;

public interface IPatService
{
Task VerifyAsync(string userId, string pat, CancellationToken cancellationToken = default);

Task<IEnumerable<IPat>> GetAllAsync(string userId, CancellationToken cancellationToken = default);

Task<string> CreateAsync(IPatCreate patCreate, CancellationToken cancellationToken = default);
}
2 changes: 1 addition & 1 deletion cloud/src/Signal.Core/Auth/IUserAuth.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@

public interface IUserAuth
{
public string UserId { get; }
string UserId { get; }
}
5 changes: 5 additions & 0 deletions cloud/src/Signal.Core/Auth/PatCreate.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
using System;

namespace Signal.Core.Auth;

public record PatCreate(string UserId, string? Alias, DateTime? Expire) : IPatCreate;
72 changes: 72 additions & 0 deletions cloud/src/Signal.Core/Auth/PatService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Net;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.IdentityModel.Tokens;
using Signal.Core.Exceptions;
using Signal.Core.Secrets;
using Signal.Core.Storage;

namespace Signal.Core.Auth;

public class PatService(
IAzureStorage storage,
IAzureStorageDao dao,
ISecretsProvider secretsProvider) : IPatService
{
public async Task VerifyAsync(string userId, string pat, CancellationToken cancellationToken = default)
{
if (!await dao.PatExistsAsync(userId, PatHashSha256(userId, pat), cancellationToken))
throw new ExpectedHttpException(HttpStatusCode.Unauthorized);
}

public Task<IEnumerable<IPat>> GetAllAsync(string userId, CancellationToken cancellationToken = default) =>
dao.PatsAsync(userId, cancellationToken);

public async Task<string> CreateAsync(IPatCreate patCreate, CancellationToken cancellationToken = default)
{
var token = await this.JwtTokenAsync(patCreate.UserId, patCreate.Expire, cancellationToken);
var hash = PatHashSha256(patCreate.UserId, token);
await storage.PatCreateAsync(
patCreate.UserId,
token[^4..], hash,
patCreate.Alias,
patCreate.Expire, cancellationToken);
return token;
}

private async Task<string> JwtTokenAsync(string userId, DateTime? expire, CancellationToken cancellationToken)
{
var signingToken = await secretsProvider.GetSecretAsync(SecretKeys.PatSigningToken, cancellationToken);
var signingKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(signingToken));
var signingCredentials = new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha512Signature);
var claims = new List<Claim>
{
new(ClaimTypes.NameIdentifier, userId),
};
var tokenDescriptor = new SecurityTokenDescriptor
{
Issuer = "signalcopat",
Subject = new ClaimsIdentity(claims),
Expires = expire,
SigningCredentials = signingCredentials
};
var tokenHandler = new JwtSecurityTokenHandler();
var token = tokenHandler.CreateToken(tokenDescriptor);
return tokenHandler.WriteToken(token);
}

private static string PatHashSha256(string key, string pat)
{
var hash = new StringBuilder();
var crypto = HMACSHA512.HashData(Encoding.UTF8.GetBytes(key), Encoding.UTF8.GetBytes(pat));
foreach (var theByte in crypto)
hash.Append(theByte.ToString("x2"));
return hash.ToString();
}
}
2 changes: 2 additions & 0 deletions cloud/src/Signal.Core/CoreExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using Microsoft.Extensions.DependencyInjection;
using Signal.Core.Auth;
using Signal.Core.Entities;
using Signal.Core.Notifications;
using Signal.Core.Sharing;
Expand All @@ -16,6 +17,7 @@ public static IServiceCollection AddCore(this IServiceCollection services) =>
.AddTransient<ISharingService, SharingService>()
.AddTransient<IEntityService, EntityService>()
.AddTransient<IUserService, UserService>()
.AddTransient<IPatService, PatService>()
.AddTransient(typeof(Lazy<>), typeof(LazyInstance<>));
}

Expand Down
2 changes: 2 additions & 0 deletions cloud/src/Signal.Core/Secrets/SecretKeys.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ public static class SecretKeys

public const string SignalRConnectionString = "AzureSignalRConnectionString";

public const string PatSigningToken = "SignalcoPatSigningToken";

public static class Auth0
{
public const string ApiIdentifier = "Auth0_ApiIdentifier";
Expand Down
1 change: 1 addition & 0 deletions cloud/src/Signal.Core/Signal.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="7.2.0" />
</ItemGroup>
</Project>
11 changes: 10 additions & 1 deletion cloud/src/Signal.Core/Storage/IAzureStorage.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.IO;
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Signal.Core.Contacts;
Expand All @@ -12,6 +13,14 @@ namespace Signal.Core.Storage;

public interface IAzureStorage
{
Task PatCreateAsync(
string userId,
string patEnd,
string patHash,
string? alias,
DateTime? expire,
CancellationToken cancellationToken = default);

Task UpsertAsync(IEntity entity, CancellationToken cancellationToken = default);

Task UpsertAsync(IContactPointer contact, CancellationToken cancellationToken = default);
Expand Down
8 changes: 8 additions & 0 deletions cloud/src/Signal.Core/Storage/IAzureStorageDao.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Signal.Core.Auth;
using Signal.Core.Contacts;
using Signal.Core.Entities;
using Signal.Core.Processor;
Expand All @@ -14,6 +15,13 @@ namespace Signal.Core.Storage;

public interface IAzureStorageDao
{
Task<bool> PatExistsAsync(
string userId,
string patHash,
CancellationToken cancellationToken = default);

Task<IEnumerable<IPat>> PatsAsync(string userId, CancellationToken cancellationToken = default);

Task<IEnumerable<IEntity>> UserEntitiesAsync(
string userId,
IEnumerable<EntityType>? types,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using System;
using Signal.Core.Auth;

namespace Signal.Infrastructure.AzureStorage.Tables;

[Serializable]
internal class AzureAuthPat : AzureTableEntityBase
{
public string PatEnd { get; }
public string? Alias { get; }
public DateTime? Expire { get; }

public AzureAuthPat(string userId, string patHash, string patEnd, string? alias, DateTime? expire)
: this(userId, patHash)
{
this.PatEnd = patEnd;
this.Alias = alias;
this.Expire = expire;
}

private AzureAuthPat(string partitionKey, string rowKey) : base(partitionKey, rowKey)
{
}

public static IPat ToPat(AzureAuthPat pat) => new Pat
{
UserId = pat.PartitionKey,
PatHash = pat.RowKey,
PatEnd = pat.PatEnd,
Alias = pat.Alias,
Expire = pat.Expire,
};
}
Loading

0 comments on commit fb309f0

Please sign in to comment.