add basic suppport for client_credentials oauth grant
This commit is contained in:
parent
049f4a56de
commit
8995213d26
20 changed files with 627 additions and 58 deletions
|
@ -30,12 +30,12 @@ public partial class RequestSigningService
|
||||||
if (!resp.IsSuccessStatusCode)
|
if (!resp.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
var error = await resp.Content.ReadAsStringAsync();
|
var error = await resp.Content.ReadAsStringAsync();
|
||||||
throw new FoxchatError.OutgoingFederationError($"Request to {domain}{requestPath} returned an error", DeserializeObject<Models.ApiError>(error));
|
throw new ApiError.OutgoingFederationError($"Request to {domain}{requestPath} returned an error", DeserializeObject<Models.ApiError>(error));
|
||||||
}
|
}
|
||||||
|
|
||||||
var bodyString = await resp.Content.ReadAsStringAsync();
|
var bodyString = await resp.Content.ReadAsStringAsync();
|
||||||
return DeserializeObject<T>(bodyString)
|
return DeserializeObject<T>(bodyString)
|
||||||
?? throw new FoxchatError.OutgoingFederationError($"Request to {domain}{requestPath} returned invalid response body");
|
?? throw new ApiError.OutgoingFederationError($"Request to {domain}{requestPath} returned invalid response body");
|
||||||
}
|
}
|
||||||
|
|
||||||
private HttpRequestMessage BuildHttpRequest(HttpMethod method, string domain, string requestPath, string? userId = null, object? bodyData = null)
|
private HttpRequestMessage BuildHttpRequest(HttpMethod method, string domain, string requestPath, string? userId = null, object? bodyData = null)
|
||||||
|
|
|
@ -5,16 +5,15 @@ using Foxchat.Core.Database;
|
||||||
using Foxchat.Core.Utils;
|
using Foxchat.Core.Utils;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
using NodaTime.Text;
|
using NodaTime.Text;
|
||||||
using Serilog;
|
|
||||||
|
|
||||||
namespace Foxchat.Core.Federation;
|
namespace Foxchat.Core.Federation;
|
||||||
|
|
||||||
public partial class RequestSigningService(ILogger logger, IClock clock, IDatabaseContext context, CoreConfig config)
|
public partial class RequestSigningService(ILogger logger, IClock clock, IDatabaseContext db, CoreConfig config)
|
||||||
{
|
{
|
||||||
private readonly ILogger _logger = logger.ForContext<RequestSigningService>();
|
private readonly ILogger _logger = logger.ForContext<RequestSigningService>();
|
||||||
private readonly IClock _clock = clock;
|
private readonly IClock _clock = clock;
|
||||||
private readonly CoreConfig _config = config;
|
private readonly CoreConfig _config = config;
|
||||||
private readonly RSA _rsa = context.GetInstanceKeysAsync().GetAwaiter().GetResult();
|
private readonly RSA _rsa = db.GetInstanceKeysAsync().GetAwaiter().GetResult();
|
||||||
private readonly HttpClient _httpClient = new();
|
private readonly HttpClient _httpClient = new();
|
||||||
|
|
||||||
public string GenerateSignature(SignatureData data)
|
public string GenerateSignature(SignatureData data)
|
||||||
|
@ -41,11 +40,11 @@ public partial class RequestSigningService(ILogger logger, IClock clock, IDataba
|
||||||
var time = ParseTime(dateHeader);
|
var time = ParseTime(dateHeader);
|
||||||
if ((now + Duration.FromMinutes(1)) < time)
|
if ((now + Duration.FromMinutes(1)) < time)
|
||||||
{
|
{
|
||||||
throw new FoxchatError.IncomingFederationError("Request was made in the future");
|
throw new ApiError.IncomingFederationError("Request was made in the future");
|
||||||
}
|
}
|
||||||
else if ((now - Duration.FromMinutes(1)) > time)
|
else if ((now - Duration.FromMinutes(1)) > time)
|
||||||
{
|
{
|
||||||
throw new FoxchatError.IncomingFederationError("Request was made too long ago");
|
throw new ApiError.IncomingFederationError("Request was made too long ago");
|
||||||
}
|
}
|
||||||
|
|
||||||
var plaintext = GeneratePlaintext(new SignatureData(time, host, requestPath, contentLength, userId));
|
var plaintext = GeneratePlaintext(new SignatureData(time, host, requestPath, contentLength, userId));
|
||||||
|
@ -54,7 +53,7 @@ public partial class RequestSigningService(ILogger logger, IClock clock, IDataba
|
||||||
|
|
||||||
if (!CryptoUtils.TryFromBase64String(encodedSignature, out var signature))
|
if (!CryptoUtils.TryFromBase64String(encodedSignature, out var signature))
|
||||||
{
|
{
|
||||||
throw new FoxchatError.IncomingFederationError("Invalid base64 signature");
|
throw new ApiError.IncomingFederationError("Invalid base64 signature");
|
||||||
}
|
}
|
||||||
|
|
||||||
var deformatter = new RSAPKCS1SignatureDeformatter(rsa);
|
var deformatter = new RSAPKCS1SignatureDeformatter(rsa);
|
||||||
|
|
|
@ -4,18 +4,8 @@ namespace Foxchat.Core;
|
||||||
|
|
||||||
public class FoxchatError(string message) : Exception(message)
|
public class FoxchatError(string message) : Exception(message)
|
||||||
{
|
{
|
||||||
public class BadRequest(string message) : ApiError(message, statusCode: HttpStatusCode.BadRequest);
|
|
||||||
public class IncomingFederationError(string message) : ApiError(message, statusCode: HttpStatusCode.BadRequest);
|
|
||||||
|
|
||||||
public class OutgoingFederationError(
|
|
||||||
string message, Models.ApiError? innerError = null
|
|
||||||
) : ApiError(message, statusCode: HttpStatusCode.InternalServerError)
|
|
||||||
{
|
|
||||||
public Models.ApiError? InnerError => innerError;
|
|
||||||
}
|
|
||||||
|
|
||||||
public class DatabaseError(string message) : FoxchatError(message);
|
public class DatabaseError(string message) : FoxchatError(message);
|
||||||
public class AuthenticationError(string message) : ApiError(message, statusCode: HttpStatusCode.BadRequest);
|
public class UnknownEntityError(Type entityType) : FoxchatError($"Entity of type {entityType.Name} not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
public class ApiError(string message, HttpStatusCode? statusCode = null) : FoxchatError(message)
|
public class ApiError(string message, HttpStatusCode? statusCode = null) : FoxchatError(message)
|
||||||
|
@ -28,4 +18,16 @@ public class ApiError(string message, HttpStatusCode? statusCode = null) : Foxch
|
||||||
{
|
{
|
||||||
public readonly string[] Scopes = scopes?.ToArray() ?? [];
|
public readonly string[] Scopes = scopes?.ToArray() ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class BadRequest(string message) : ApiError(message, statusCode: HttpStatusCode.BadRequest);
|
||||||
|
public class IncomingFederationError(string message) : ApiError(message, statusCode: HttpStatusCode.BadRequest);
|
||||||
|
|
||||||
|
public class OutgoingFederationError(
|
||||||
|
string message, Models.ApiError? innerError = null
|
||||||
|
) : ApiError(message, statusCode: HttpStatusCode.InternalServerError)
|
||||||
|
{
|
||||||
|
public Models.ApiError? InnerError => innerError;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class AuthenticationError(string message) : ApiError(message, statusCode: HttpStatusCode.BadRequest);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +0,0 @@
|
||||||
namespace Foxchat.Core.Models;
|
|
||||||
|
|
||||||
public record HelloRequest(string Host);
|
|
||||||
public record HelloResponse(string PublicKey, string Host);
|
|
||||||
public record NodeInfo(string Software, string PublicKey);
|
|
||||||
public record NodeSoftware(string Name, string? Version);
|
|
8
Foxchat.Core/Models/Http/Apps.cs
Normal file
8
Foxchat.Core/Models/Http/Apps.cs
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
namespace Foxchat.Core.Models.Http;
|
||||||
|
|
||||||
|
public static class Apps
|
||||||
|
{
|
||||||
|
public record CreateRequest(string Name, string[] Scopes, string[] RedirectUris);
|
||||||
|
public record CreateResponse(Ulid Id, string ClientId, string ClientSecret, string Name, string[] Scopes, string[] RedirectUris);
|
||||||
|
public record GetSelfResponse(Ulid Id, string ClientId, string? ClientSecret, string Name, string[] Scopes, string[] RedirectUris);
|
||||||
|
}
|
9
Foxchat.Core/Models/Http/Hello.cs
Normal file
9
Foxchat.Core/Models/Http/Hello.cs
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
namespace Foxchat.Core.Models.Http;
|
||||||
|
|
||||||
|
public static class Hello
|
||||||
|
{
|
||||||
|
public record HelloRequest(string Host);
|
||||||
|
public record HelloResponse(string PublicKey, string Host);
|
||||||
|
public record NodeInfo(string Software, string PublicKey);
|
||||||
|
public record NodeSoftware(string Name, string? Version);
|
||||||
|
}
|
|
@ -63,7 +63,7 @@ public static class HttpContextExtensions
|
||||||
public static void SetToken(this HttpContext ctx, Token token) => ctx.Items.Add(Key, token);
|
public static void SetToken(this HttpContext ctx, Token token) => ctx.Items.Add(Key, token);
|
||||||
public static Account? GetAccount(this HttpContext ctx) => ctx.GetToken()?.Account;
|
public static Account? GetAccount(this HttpContext ctx) => ctx.GetToken()?.Account;
|
||||||
public static Account GetAccountOrThrow(this HttpContext ctx) =>
|
public static Account GetAccountOrThrow(this HttpContext ctx) =>
|
||||||
ctx.GetAccount() ?? throw new FoxchatError.AuthenticationError("No account in HttpContext");
|
ctx.GetAccount() ?? throw new ApiError.AuthenticationError("No account in HttpContext");
|
||||||
|
|
||||||
public static Token? GetToken(this HttpContext ctx)
|
public static Token? GetToken(this HttpContext ctx)
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
using Foxchat.Core.Models;
|
using Foxchat.Core.Models.Http;
|
||||||
using Foxchat.Identity.Database;
|
using Foxchat.Identity.Database;
|
||||||
using Foxchat.Identity.Services;
|
using Foxchat.Identity.Services;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
@ -7,15 +7,15 @@ namespace Foxchat.Identity.Controllers;
|
||||||
|
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("/_fox/ident/node")]
|
[Route("/_fox/ident/node")]
|
||||||
public class NodeController(IdentityContext context, ChatInstanceResolverService chatInstanceResolverService) : ControllerBase
|
public class NodeController(IdentityContext db, ChatInstanceResolverService chatInstanceResolverService) : ControllerBase
|
||||||
{
|
{
|
||||||
public const string SOFTWARE_NAME = "Foxchat.NET.Identity";
|
public const string SOFTWARE_NAME = "Foxchat.NET.Identity";
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
public async Task<IActionResult> GetNode()
|
public async Task<IActionResult> GetNode()
|
||||||
{
|
{
|
||||||
var instance = await context.GetInstanceAsync();
|
var instance = await db.GetInstanceAsync();
|
||||||
return Ok(new NodeInfo(SOFTWARE_NAME, instance.PublicKey));
|
return Ok(new Hello.NodeInfo(SOFTWARE_NAME, instance.PublicKey));
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{domain}")]
|
[HttpGet("{domain}")]
|
||||||
|
|
45
Foxchat.Identity/Controllers/Oauth/AppsController.cs
Normal file
45
Foxchat.Identity/Controllers/Oauth/AppsController.cs
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
using Foxchat.Core;
|
||||||
|
using Foxchat.Core.Models.Http;
|
||||||
|
using Foxchat.Identity.Authorization;
|
||||||
|
using Foxchat.Identity.Database;
|
||||||
|
using Foxchat.Identity.Database.Models;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace Foxchat.Identity.Controllers.Oauth;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Authenticate]
|
||||||
|
[Route("/_fox/ident/oauth/apps")]
|
||||||
|
public class AppsController(ILogger logger, IdentityContext db) : ControllerBase
|
||||||
|
{
|
||||||
|
[HttpPost]
|
||||||
|
public async Task<IActionResult> CreateApplication([FromBody] Apps.CreateRequest req)
|
||||||
|
{
|
||||||
|
var app = Application.Create(req.Name, req.Scopes, req.RedirectUris);
|
||||||
|
await db.AddAsync(app);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
logger.Information("Created new application {Name} with ID {Id} and client ID {ClientId}", app.Name, app.Id, app.ClientId);
|
||||||
|
|
||||||
|
return Ok(new Apps.CreateResponse(
|
||||||
|
app.Id, app.ClientId, app.ClientSecret, app.Name, app.Scopes, app.RedirectUris
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public IActionResult GetSelfApp([FromQuery(Name = "with_secret")] bool withSecret)
|
||||||
|
{
|
||||||
|
var token = HttpContext.GetToken();
|
||||||
|
if (token is not { Account: null }) throw new ApiError.Forbidden("This endpoint requires a client token.");
|
||||||
|
var app = token.Application;
|
||||||
|
|
||||||
|
return Ok(new Apps.GetSelfResponse(
|
||||||
|
app.Id,
|
||||||
|
app.ClientId,
|
||||||
|
withSecret ? app.ClientSecret : null,
|
||||||
|
app.Name,
|
||||||
|
app.Scopes,
|
||||||
|
app.RedirectUris
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
73
Foxchat.Identity/Controllers/Oauth/TokenController.cs
Normal file
73
Foxchat.Identity/Controllers/Oauth/TokenController.cs
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
using Foxchat.Core;
|
||||||
|
using Foxchat.Identity.Database;
|
||||||
|
using Foxchat.Identity.Database.Models;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using NodaTime;
|
||||||
|
|
||||||
|
namespace Foxchat.Identity.Controllers.Oauth;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("/_fox/ident/oauth/token")]
|
||||||
|
public class TokenController(ILogger logger, IdentityContext db, IClock clock) : ControllerBase
|
||||||
|
{
|
||||||
|
[HttpPost]
|
||||||
|
public async Task<IActionResult> PostToken([FromBody] PostTokenRequest req)
|
||||||
|
{
|
||||||
|
var app = await db.GetApplicationAsync(req.ClientId, req.ClientSecret);
|
||||||
|
|
||||||
|
var scopes = req.Scope.Split(' ');
|
||||||
|
if (app.Scopes.Except(scopes).Any())
|
||||||
|
{
|
||||||
|
throw new ApiError.BadRequest("Invalid or unauthorized scopes");
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (req.GrantType)
|
||||||
|
{
|
||||||
|
case "client_credentials":
|
||||||
|
return await HandleClientCredentialsAsync(app, scopes);
|
||||||
|
case "authorization_code":
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new ApiError.BadRequest("Unknown grant_type");
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<IActionResult> HandleClientCredentialsAsync(Application app, string[] scopes)
|
||||||
|
{
|
||||||
|
// TODO: make this configurable
|
||||||
|
var expiry = clock.GetCurrentInstant() + Duration.FromDays(365);
|
||||||
|
var (token, hash) = Token.Generate();
|
||||||
|
var tokenObj = new Token
|
||||||
|
{
|
||||||
|
Hash = hash,
|
||||||
|
Scopes = scopes,
|
||||||
|
Expires = expiry,
|
||||||
|
ApplicationId = app.Id
|
||||||
|
};
|
||||||
|
|
||||||
|
await db.AddAsync(tokenObj);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
logger.Debug("Created token with scopes {Scopes} for application {ApplicationId}", scopes, app.Id);
|
||||||
|
|
||||||
|
return Ok(new PostTokenResponse(token, scopes, expiry));
|
||||||
|
}
|
||||||
|
|
||||||
|
public record PostTokenRequest(
|
||||||
|
string GrantType,
|
||||||
|
string ClientId,
|
||||||
|
string ClientSecret,
|
||||||
|
string Scope,
|
||||||
|
// Optional parameters
|
||||||
|
string? Code,
|
||||||
|
string? RedirectUri
|
||||||
|
);
|
||||||
|
|
||||||
|
public record PostTokenResponse(
|
||||||
|
string Token,
|
||||||
|
string[] Scopes,
|
||||||
|
Instant Expires
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,5 +1,8 @@
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
|
using Foxchat.Core;
|
||||||
|
using Foxchat.Identity.Utils;
|
||||||
using Microsoft.AspNetCore.WebUtilities;
|
using Microsoft.AspNetCore.WebUtilities;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace Foxchat.Identity.Database.Models;
|
namespace Foxchat.Identity.Database.Models;
|
||||||
|
|
||||||
|
@ -9,38 +12,47 @@ public class Application : BaseModel
|
||||||
public required string ClientSecret { get; init; }
|
public required string ClientSecret { get; init; }
|
||||||
public required string Name { get; init; }
|
public required string Name { get; init; }
|
||||||
public required string[] Scopes { get; init; }
|
public required string[] Scopes { get; init; }
|
||||||
|
public required string[] RedirectUris { get; set; }
|
||||||
|
|
||||||
public static Application Create(string name, string[] scopes)
|
public static Application Create(string name, string[] scopes, string[] redirectUrls)
|
||||||
{
|
{
|
||||||
var clientId = RandomNumberGenerator.GetHexString(16, true);
|
var clientId = RandomNumberGenerator.GetHexString(32, true);
|
||||||
var clientSecretBytes = RandomNumberGenerator.GetBytes(48);
|
var clientSecretBytes = RandomNumberGenerator.GetBytes(48);
|
||||||
var clientSecret = WebEncoders.Base64UrlEncode(clientSecretBytes);
|
var clientSecret = WebEncoders.Base64UrlEncode(clientSecretBytes);
|
||||||
|
|
||||||
if (!scopes.All(s => Scope.ValidScopes.Contains(s)))
|
if (scopes.Any(s => !OauthUtils.Scopes.Contains(s)))
|
||||||
{
|
{
|
||||||
throw new ArgumentException("Invalid scopes passed to Application.Create", nameof(scopes));
|
throw new ArgumentException("Invalid scopes passed to Application.Create", nameof(scopes));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (redirectUrls.Any(s => !OauthUtils.ValidateRedirectUri(s)))
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Invalid redirect URLs passed to Application.Create", nameof(redirectUrls));
|
||||||
|
}
|
||||||
|
|
||||||
return new Application
|
return new Application
|
||||||
{
|
{
|
||||||
ClientId = clientId,
|
ClientId = clientId,
|
||||||
ClientSecret = clientSecret,
|
ClientSecret = clientSecret,
|
||||||
Name = name,
|
Name = name,
|
||||||
Scopes = scopes,
|
Scopes = scopes,
|
||||||
|
RedirectUris = redirectUrls
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class Scope
|
public static class ContextApplicationExtensions
|
||||||
{
|
{
|
||||||
/// <summary>
|
public static async Task<Application> GetApplicationAsync(this IdentityContext db, string clientId)
|
||||||
/// OAuth scope for identifying a user and nothing else.
|
{
|
||||||
/// </summary>
|
return await db.Applications.FirstOrDefaultAsync(a => a.ClientId == clientId)
|
||||||
public const string Identity = "identity";
|
?? throw new ApiError.Unauthorized("Invalid client ID or client secret");
|
||||||
/// <summary>
|
}
|
||||||
/// OAuth scope for a full chat client. This grants *full access* to an account.
|
|
||||||
/// </summary>
|
public static async Task<Application> GetApplicationAsync(this IdentityContext db, string clientId, string clientSecret)
|
||||||
public const string ChatClient = "chat_client";
|
{
|
||||||
|
var app = await db.GetApplicationAsync(clientId);
|
||||||
public static readonly string[] ValidScopes = [Identity, ChatClient];
|
if (app.ClientSecret != clientSecret) throw new ApiError.Unauthorized("Invalid client ID or client secret");
|
||||||
|
return app;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,8 +10,9 @@ public class Token : BaseModel
|
||||||
public string[] Scopes { get; set; } = [];
|
public string[] Scopes { get; set; } = [];
|
||||||
public Instant Expires { get; set; }
|
public Instant Expires { get; set; }
|
||||||
|
|
||||||
public Ulid AccountId { get; set; }
|
// Tokens can be granted directly to applications with `client_credentials`
|
||||||
public Account Account { get; set; } = null!;
|
public Ulid? AccountId { get; set; }
|
||||||
|
public Account? Account { get; set; }
|
||||||
|
|
||||||
public Ulid ApplicationId { get; set; }
|
public Ulid ApplicationId { get; set; }
|
||||||
public Application Application { get; set; } = null!;
|
public Application Application { get; set; } = null!;
|
||||||
|
|
|
@ -4,6 +4,13 @@ namespace Foxchat.Identity.Extensions;
|
||||||
|
|
||||||
public static class WebApplicationExtensions
|
public static class WebApplicationExtensions
|
||||||
{
|
{
|
||||||
|
public static IServiceCollection AddCustomMiddleware(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
return services
|
||||||
|
.AddScoped<AuthenticationMiddleware>()
|
||||||
|
.AddScoped<AuthorizationMiddleware>();
|
||||||
|
}
|
||||||
|
|
||||||
public static IApplicationBuilder UseCustomMiddleware(this IApplicationBuilder app)
|
public static IApplicationBuilder UseCustomMiddleware(this IApplicationBuilder app)
|
||||||
{
|
{
|
||||||
return app
|
return app
|
||||||
|
|
323
Foxchat.Identity/Migrations/20240520141853_AddRedirectUris.Designer.cs
generated
Normal file
323
Foxchat.Identity/Migrations/20240520141853_AddRedirectUris.Designer.cs
generated
Normal file
|
@ -0,0 +1,323 @@
|
||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Foxchat.Identity.Database;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using NodaTime;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Foxchat.Identity.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(IdentityContext))]
|
||||||
|
[Migration("20240520141853_AddRedirectUris")]
|
||||||
|
partial class AddRedirectUris
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "8.0.4")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("AccountChatInstance", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("AccountsId")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("accounts_id");
|
||||||
|
|
||||||
|
b.Property<Guid>("ChatInstancesId")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("chat_instances_id");
|
||||||
|
|
||||||
|
b.HasKey("AccountsId", "ChatInstancesId")
|
||||||
|
.HasName("pk_account_chat_instance");
|
||||||
|
|
||||||
|
b.HasIndex("ChatInstancesId")
|
||||||
|
.HasDatabaseName("ix_account_chat_instance_chat_instances_id");
|
||||||
|
|
||||||
|
b.ToTable("account_chat_instance", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Foxchat.Core.Database.Instance", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("PrivateKey")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("private_key");
|
||||||
|
|
||||||
|
b.Property<string>("PublicKey")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("public_key");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("pk_instance");
|
||||||
|
|
||||||
|
b.ToTable("instance", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Foxchat.Identity.Database.Models.Account", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<string>("Avatar")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("avatar");
|
||||||
|
|
||||||
|
b.Property<string>("Email")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("email");
|
||||||
|
|
||||||
|
b.Property<string>("Password")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("password");
|
||||||
|
|
||||||
|
b.Property<int>("Role")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("role");
|
||||||
|
|
||||||
|
b.Property<string>("Username")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("username");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("pk_accounts");
|
||||||
|
|
||||||
|
b.HasIndex("Email")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("ix_accounts_email");
|
||||||
|
|
||||||
|
b.HasIndex("Username")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("ix_accounts_username");
|
||||||
|
|
||||||
|
b.ToTable("accounts", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Foxchat.Identity.Database.Models.Application", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<string>("ClientId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("client_id");
|
||||||
|
|
||||||
|
b.Property<string>("ClientSecret")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("client_secret");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("name");
|
||||||
|
|
||||||
|
b.Property<string[]>("RedirectUris")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text[]")
|
||||||
|
.HasColumnName("redirect_uris");
|
||||||
|
|
||||||
|
b.Property<string[]>("Scopes")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text[]")
|
||||||
|
.HasColumnName("scopes");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("pk_applications");
|
||||||
|
|
||||||
|
b.HasIndex("ClientId")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("ix_applications_client_id");
|
||||||
|
|
||||||
|
b.ToTable("applications", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Foxchat.Identity.Database.Models.ChatInstance", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<string>("BaseUrl")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("base_url");
|
||||||
|
|
||||||
|
b.Property<string>("Domain")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("domain");
|
||||||
|
|
||||||
|
b.Property<string>("PublicKey")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("public_key");
|
||||||
|
|
||||||
|
b.Property<string>("Reason")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("reason");
|
||||||
|
|
||||||
|
b.Property<int>("Status")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("status");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("pk_chat_instances");
|
||||||
|
|
||||||
|
b.HasIndex("Domain")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("ix_chat_instances_domain");
|
||||||
|
|
||||||
|
b.ToTable("chat_instances", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Foxchat.Identity.Database.Models.GuildAccount", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("ChatInstanceId")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("chat_instance_id");
|
||||||
|
|
||||||
|
b.Property<string>("GuildId")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("guild_id");
|
||||||
|
|
||||||
|
b.Property<Guid>("AccountId")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("account_id");
|
||||||
|
|
||||||
|
b.HasKey("ChatInstanceId", "GuildId", "AccountId")
|
||||||
|
.HasName("pk_guild_accounts");
|
||||||
|
|
||||||
|
b.HasIndex("AccountId")
|
||||||
|
.HasDatabaseName("ix_guild_accounts_account_id");
|
||||||
|
|
||||||
|
b.ToTable("guild_accounts", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Foxchat.Identity.Database.Models.Token", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<Guid?>("AccountId")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("account_id");
|
||||||
|
|
||||||
|
b.Property<Guid>("ApplicationId")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("application_id");
|
||||||
|
|
||||||
|
b.Property<Instant>("Expires")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("expires");
|
||||||
|
|
||||||
|
b.Property<byte[]>("Hash")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("bytea")
|
||||||
|
.HasColumnName("hash");
|
||||||
|
|
||||||
|
b.Property<string[]>("Scopes")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text[]")
|
||||||
|
.HasColumnName("scopes");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("pk_tokens");
|
||||||
|
|
||||||
|
b.HasIndex("AccountId")
|
||||||
|
.HasDatabaseName("ix_tokens_account_id");
|
||||||
|
|
||||||
|
b.HasIndex("ApplicationId")
|
||||||
|
.HasDatabaseName("ix_tokens_application_id");
|
||||||
|
|
||||||
|
b.ToTable("tokens", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("AccountChatInstance", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Foxchat.Identity.Database.Models.Account", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("AccountsId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired()
|
||||||
|
.HasConstraintName("fk_account_chat_instance_accounts_accounts_id");
|
||||||
|
|
||||||
|
b.HasOne("Foxchat.Identity.Database.Models.ChatInstance", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("ChatInstancesId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired()
|
||||||
|
.HasConstraintName("fk_account_chat_instance_chat_instances_chat_instances_id");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Foxchat.Identity.Database.Models.GuildAccount", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Foxchat.Identity.Database.Models.Account", "Account")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("AccountId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired()
|
||||||
|
.HasConstraintName("fk_guild_accounts_accounts_account_id");
|
||||||
|
|
||||||
|
b.HasOne("Foxchat.Identity.Database.Models.ChatInstance", "ChatInstance")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("ChatInstanceId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired()
|
||||||
|
.HasConstraintName("fk_guild_accounts_chat_instances_chat_instance_id");
|
||||||
|
|
||||||
|
b.Navigation("Account");
|
||||||
|
|
||||||
|
b.Navigation("ChatInstance");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Foxchat.Identity.Database.Models.Token", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Foxchat.Identity.Database.Models.Account", "Account")
|
||||||
|
.WithMany("Tokens")
|
||||||
|
.HasForeignKey("AccountId")
|
||||||
|
.HasConstraintName("fk_tokens_accounts_account_id");
|
||||||
|
|
||||||
|
b.HasOne("Foxchat.Identity.Database.Models.Application", "Application")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("ApplicationId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired()
|
||||||
|
.HasConstraintName("fk_tokens_applications_application_id");
|
||||||
|
|
||||||
|
b.Navigation("Account");
|
||||||
|
|
||||||
|
b.Navigation("Application");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Foxchat.Identity.Database.Models.Account", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Tokens");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,71 @@
|
||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Foxchat.Identity.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddRedirectUris : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "fk_tokens_accounts_account_id",
|
||||||
|
table: "tokens");
|
||||||
|
|
||||||
|
migrationBuilder.AlterColumn<Guid>(
|
||||||
|
name: "account_id",
|
||||||
|
table: "tokens",
|
||||||
|
type: "uuid",
|
||||||
|
nullable: true,
|
||||||
|
oldClrType: typeof(Guid),
|
||||||
|
oldType: "uuid");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string[]>(
|
||||||
|
name: "redirect_uris",
|
||||||
|
table: "applications",
|
||||||
|
type: "text[]",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: new string[0]);
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "fk_tokens_accounts_account_id",
|
||||||
|
table: "tokens",
|
||||||
|
column: "account_id",
|
||||||
|
principalTable: "accounts",
|
||||||
|
principalColumn: "id");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "fk_tokens_accounts_account_id",
|
||||||
|
table: "tokens");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "redirect_uris",
|
||||||
|
table: "applications");
|
||||||
|
|
||||||
|
migrationBuilder.AlterColumn<Guid>(
|
||||||
|
name: "account_id",
|
||||||
|
table: "tokens",
|
||||||
|
type: "uuid",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: new Guid("00000000-0000-0000-0000-000000000000"),
|
||||||
|
oldClrType: typeof(Guid),
|
||||||
|
oldType: "uuid",
|
||||||
|
oldNullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "fk_tokens_accounts_account_id",
|
||||||
|
table: "tokens",
|
||||||
|
column: "account_id",
|
||||||
|
principalTable: "accounts",
|
||||||
|
principalColumn: "id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -131,6 +131,11 @@ namespace Foxchat.Identity.Migrations
|
||||||
.HasColumnType("text")
|
.HasColumnType("text")
|
||||||
.HasColumnName("name");
|
.HasColumnName("name");
|
||||||
|
|
||||||
|
b.Property<string[]>("RedirectUris")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text[]")
|
||||||
|
.HasColumnName("redirect_uris");
|
||||||
|
|
||||||
b.Property<string[]>("Scopes")
|
b.Property<string[]>("Scopes")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("text[]")
|
.HasColumnType("text[]")
|
||||||
|
@ -214,7 +219,7 @@ namespace Foxchat.Identity.Migrations
|
||||||
.HasColumnType("uuid")
|
.HasColumnType("uuid")
|
||||||
.HasColumnName("id");
|
.HasColumnName("id");
|
||||||
|
|
||||||
b.Property<Guid>("AccountId")
|
b.Property<Guid?>("AccountId")
|
||||||
.HasColumnType("uuid")
|
.HasColumnType("uuid")
|
||||||
.HasColumnName("account_id");
|
.HasColumnName("account_id");
|
||||||
|
|
||||||
|
@ -291,8 +296,6 @@ namespace Foxchat.Identity.Migrations
|
||||||
b.HasOne("Foxchat.Identity.Database.Models.Account", "Account")
|
b.HasOne("Foxchat.Identity.Database.Models.Account", "Account")
|
||||||
.WithMany("Tokens")
|
.WithMany("Tokens")
|
||||||
.HasForeignKey("AccountId")
|
.HasForeignKey("AccountId")
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired()
|
|
||||||
.HasConstraintName("fk_tokens_accounts_account_id");
|
.HasConstraintName("fk_tokens_accounts_account_id");
|
||||||
|
|
||||||
b.HasOne("Foxchat.Identity.Database.Models.Application", "Application")
|
b.HasOne("Foxchat.Identity.Database.Models.Application", "Application")
|
||||||
|
|
|
@ -26,19 +26,18 @@ builder.Services
|
||||||
builder.Services
|
builder.Services
|
||||||
.AddCoreServices<IdentityContext>()
|
.AddCoreServices<IdentityContext>()
|
||||||
.AddScoped<ChatInstanceResolverService>()
|
.AddScoped<ChatInstanceResolverService>()
|
||||||
|
.AddCustomMiddleware()
|
||||||
.AddEndpointsApiExplorer()
|
.AddEndpointsApiExplorer()
|
||||||
.AddSwaggerGen();
|
.AddSwaggerGen();
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
app.UseSerilogRequestLogging();
|
app.UseSerilogRequestLogging();
|
||||||
app.UseCustomMiddleware();
|
|
||||||
app.UseRouting();
|
app.UseRouting();
|
||||||
app.UseSwagger();
|
app.UseSwagger();
|
||||||
app.UseSwaggerUI();
|
app.UseSwaggerUI();
|
||||||
app.UseCors();
|
app.UseCors();
|
||||||
app.UseAuthentication();
|
app.UseCustomMiddleware();
|
||||||
app.UseAuthorization();
|
|
||||||
app.MapControllers();
|
app.MapControllers();
|
||||||
|
|
||||||
using (var scope = app.Services.CreateScope())
|
using (var scope = app.Services.CreateScope())
|
||||||
|
|
|
@ -1,27 +1,27 @@
|
||||||
using Foxchat.Core.Federation;
|
using Foxchat.Core.Federation;
|
||||||
using Foxchat.Core.Models;
|
using Foxchat.Core.Models.Http;
|
||||||
using Foxchat.Identity.Database;
|
using Foxchat.Identity.Database;
|
||||||
using Foxchat.Identity.Database.Models;
|
using Foxchat.Identity.Database.Models;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace Foxchat.Identity.Services;
|
namespace Foxchat.Identity.Services;
|
||||||
|
|
||||||
public class ChatInstanceResolverService(ILogger logger, RequestSigningService requestSigningService, IdentityContext context, InstanceConfig config)
|
public class ChatInstanceResolverService(ILogger logger, RequestSigningService requestSigningService, IdentityContext db, InstanceConfig config)
|
||||||
{
|
{
|
||||||
private readonly ILogger _logger = logger.ForContext<ChatInstanceResolverService>();
|
private readonly ILogger _logger = logger.ForContext<ChatInstanceResolverService>();
|
||||||
|
|
||||||
public async Task<ChatInstance> ResolveChatInstanceAsync(string domain)
|
public async Task<ChatInstance> ResolveChatInstanceAsync(string domain)
|
||||||
{
|
{
|
||||||
var instance = await context.ChatInstances.Where(c => c.Domain == domain).FirstOrDefaultAsync();
|
var instance = await db.ChatInstances.Where(c => c.Domain == domain).FirstOrDefaultAsync();
|
||||||
if (instance != null) return instance;
|
if (instance != null) return instance;
|
||||||
|
|
||||||
_logger.Information("Unknown chat instance {Domain}, fetching its data", domain);
|
_logger.Information("Unknown chat instance {Domain}, fetching its data", domain);
|
||||||
|
|
||||||
var resp = await requestSigningService.RequestAsync<HelloResponse>(
|
var resp = await requestSigningService.RequestAsync<Hello.HelloResponse>(
|
||||||
HttpMethod.Post,
|
HttpMethod.Post,
|
||||||
domain, "/_fox/chat/hello",
|
domain, "/_fox/chat/hello",
|
||||||
userId: null,
|
userId: null,
|
||||||
body: new HelloRequest(config.Domain)
|
body: new Hello.HelloRequest(config.Domain)
|
||||||
);
|
);
|
||||||
|
|
||||||
instance = new ChatInstance
|
instance = new ChatInstance
|
||||||
|
@ -31,8 +31,8 @@ public class ChatInstanceResolverService(ILogger logger, RequestSigningService r
|
||||||
PublicKey = resp.PublicKey,
|
PublicKey = resp.PublicKey,
|
||||||
Status = ChatInstance.InstanceStatus.Active,
|
Status = ChatInstance.InstanceStatus.Active,
|
||||||
};
|
};
|
||||||
await context.AddAsync(instance);
|
await db.AddAsync(instance);
|
||||||
await context.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
23
Foxchat.Identity/Utils/OauthUtils.cs
Normal file
23
Foxchat.Identity/Utils/OauthUtils.cs
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
namespace Foxchat.Identity.Utils;
|
||||||
|
|
||||||
|
public static class OauthUtils
|
||||||
|
{
|
||||||
|
public static readonly string[] Scopes = ["identify", "chat_client"];
|
||||||
|
|
||||||
|
private static readonly string[] ForbiddenSchemes = ["javascript", "file", "data", "mailto", "tel"];
|
||||||
|
private const string OobUri = "urn:ietf:wg:oauth:2.0:oob";
|
||||||
|
|
||||||
|
public static bool ValidateRedirectUri(string uri)
|
||||||
|
{
|
||||||
|
if (uri == OobUri) return true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var scheme = new Uri(uri).Scheme;
|
||||||
|
return !ForbiddenSchemes.Contains(scheme);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue