✨ Account and auth protobuf and client code
This commit is contained in:
22
DysonNetwork.Shared/DysonNetwork.Shared.csproj
Normal file
22
DysonNetwork.Shared/DysonNetwork.Shared.csproj
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Protobuf Include="Protos\*.proto" GrpcServices="Client" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Grpc.Net.Client" Version="2.65.0" />
|
||||||
|
<PackageReference Include="Google.Protobuf" Version="3.27.2" />
|
||||||
|
<PackageReference Include="Grpc.Tools" Version="2.65.0">
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
</PackageReference>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
28
DysonNetwork.Shared/Protos/account.proto
Normal file
28
DysonNetwork.Shared/Protos/account.proto
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
package dyson_network.sphere.account;
|
||||||
|
|
||||||
|
import "google/protobuf/empty.proto";
|
||||||
|
|
||||||
|
option csharp_namespace = "DysonNetwork.Sphere.Account.Proto";
|
||||||
|
|
||||||
|
service AccountService {
|
||||||
|
// Retrieves the current user's account information
|
||||||
|
rpc GetAccount(google.protobuf.Empty) returns (AccountResponse);
|
||||||
|
|
||||||
|
// Updates the current user's account information
|
||||||
|
rpc UpdateAccount(UpdateAccountRequest) returns (AccountResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
message AccountResponse {
|
||||||
|
string id = 1;
|
||||||
|
string username = 2;
|
||||||
|
string email = 3;
|
||||||
|
string display_name = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message UpdateAccountRequest {
|
||||||
|
// Fields to update
|
||||||
|
optional string email = 1;
|
||||||
|
optional string display_name = 2;
|
||||||
|
}
|
55
DysonNetwork.Shared/Protos/auth.proto
Normal file
55
DysonNetwork.Shared/Protos/auth.proto
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
package dyson_network.sphere.auth;
|
||||||
|
|
||||||
|
import "google/protobuf/empty.proto";
|
||||||
|
import "google/protobuf/timestamp.proto";
|
||||||
|
|
||||||
|
option csharp_namespace = "DysonNetwork.Sphere.Auth.Proto";
|
||||||
|
|
||||||
|
service AuthService {
|
||||||
|
// Standard username/password login
|
||||||
|
rpc Login(LoginRequest) returns (LoginResponse);
|
||||||
|
|
||||||
|
// Introspects an OAuth 2.0 access token.
|
||||||
|
rpc IntrospectToken(IntrospectTokenRequest) returns (IntrospectionResponse);
|
||||||
|
|
||||||
|
// Logs out the current session
|
||||||
|
rpc Logout(google.protobuf.Empty) returns (google.protobuf.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
message LoginRequest {
|
||||||
|
string username = 1;
|
||||||
|
string password = 2;
|
||||||
|
// Optional: for 2FA
|
||||||
|
string two_factor_code = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message LoginResponse {
|
||||||
|
string access_token = 1;
|
||||||
|
string refresh_token = 2;
|
||||||
|
int64 expires_in = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message IntrospectTokenRequest {
|
||||||
|
string token = 1;
|
||||||
|
// Optional: token_type_hint can be "access_token" or "refresh_token"
|
||||||
|
string token_type_hint = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message IntrospectionResponse {
|
||||||
|
// Indicates whether or not the token is currently active.
|
||||||
|
bool active = 1;
|
||||||
|
// A JSON string containing the claims of the token.
|
||||||
|
string claims = 2;
|
||||||
|
// The client identifier for the OAuth 2.0 client that requested the token.
|
||||||
|
string client_id = 3;
|
||||||
|
// The username of the resource owner who authorized the token.
|
||||||
|
string username = 4;
|
||||||
|
// The scope of the access token.
|
||||||
|
string scope = 5;
|
||||||
|
// The time at which the token was issued.
|
||||||
|
google.protobuf.Timestamp iat = 6;
|
||||||
|
// The time at which the token expires.
|
||||||
|
google.protobuf.Timestamp exp = 7;
|
||||||
|
}
|
87
DysonNetwork.Sphere/Account/AccountGrpcService.cs
Normal file
87
DysonNetwork.Sphere/Account/AccountGrpcService.cs
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
using DysonNetwork.Sphere.Account.Proto;
|
||||||
|
using Grpc.Core;
|
||||||
|
using Google.Protobuf.WellKnownTypes;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using DysonNetwork.Sphere.Auth;
|
||||||
|
|
||||||
|
namespace DysonNetwork.Sphere.Account;
|
||||||
|
|
||||||
|
public class AccountGrpcService : DysonNetwork.Sphere.Account.Proto.AccountService.AccountServiceBase
|
||||||
|
{
|
||||||
|
private readonly AppDatabase _db;
|
||||||
|
private readonly AuthService _auth;
|
||||||
|
|
||||||
|
public AccountGrpcService(AppDatabase db, AuthService auth)
|
||||||
|
{
|
||||||
|
_db = db;
|
||||||
|
_auth = auth;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<AccountResponse> GetAccount(Empty request, ServerCallContext context)
|
||||||
|
{
|
||||||
|
var account = await GetAccountFromContext(context);
|
||||||
|
return ToAccountResponse(account);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<AccountResponse> UpdateAccount(UpdateAccountRequest request, ServerCallContext context)
|
||||||
|
{
|
||||||
|
var account = await GetAccountFromContext(context);
|
||||||
|
|
||||||
|
if (request.Email != null)
|
||||||
|
{
|
||||||
|
var emailContact = await _db.AccountContacts.FirstOrDefaultAsync(c => c.AccountId == account.Id && c.Type == AccountContactType.Email);
|
||||||
|
if (emailContact != null)
|
||||||
|
{
|
||||||
|
emailContact.Content = request.Email;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
account.Contacts.Add(new AccountContact { Type = AccountContactType.Email, Content = request.Email });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.DisplayName != null)
|
||||||
|
{
|
||||||
|
account.Nick = request.DisplayName;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _db.SaveChangesAsync();
|
||||||
|
|
||||||
|
return ToAccountResponse(account);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<DysonNetwork.Sphere.Account.Account> GetAccountFromContext(ServerCallContext context)
|
||||||
|
{
|
||||||
|
var authorizationHeader = context.RequestHeaders.FirstOrDefault(h => h.Key == "authorization");
|
||||||
|
if (authorizationHeader == null)
|
||||||
|
{
|
||||||
|
throw new RpcException(new Grpc.Core.Status(StatusCode.Unauthenticated, "Missing authorization header."));
|
||||||
|
}
|
||||||
|
|
||||||
|
var token = authorizationHeader.Value.Replace("Bearer ", "");
|
||||||
|
if (!_auth.ValidateToken(token, out var sessionId))
|
||||||
|
{
|
||||||
|
throw new RpcException(new Grpc.Core.Status(StatusCode.Unauthenticated, "Invalid token."));
|
||||||
|
}
|
||||||
|
|
||||||
|
var session = await _db.AuthSessions.Include(s => s.Account).ThenInclude(a => a.Contacts).FirstOrDefaultAsync(s => s.Id == sessionId);
|
||||||
|
if (session == null)
|
||||||
|
{
|
||||||
|
throw new RpcException(new Grpc.Core.Status(StatusCode.Unauthenticated, "Session not found."));
|
||||||
|
}
|
||||||
|
|
||||||
|
return session.Account;
|
||||||
|
}
|
||||||
|
|
||||||
|
private AccountResponse ToAccountResponse(DysonNetwork.Sphere.Account.Account account)
|
||||||
|
{
|
||||||
|
var emailContact = account.Contacts.FirstOrDefault(c => c.Type == AccountContactType.Email);
|
||||||
|
return new AccountResponse
|
||||||
|
{
|
||||||
|
Id = account.Id.ToString(),
|
||||||
|
Username = account.Name,
|
||||||
|
Email = emailContact?.Content ?? "",
|
||||||
|
DisplayName = account.Nick
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
103
DysonNetwork.Sphere/Auth/AuthGrpcService.cs
Normal file
103
DysonNetwork.Sphere/Auth/AuthGrpcService.cs
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
using DysonNetwork.Sphere.Auth.Proto;
|
||||||
|
using Grpc.Core;
|
||||||
|
using Google.Protobuf.WellKnownTypes;
|
||||||
|
using DysonNetwork.Sphere.Account;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using NodaTime;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace DysonNetwork.Sphere.Auth;
|
||||||
|
|
||||||
|
public class AuthGrpcService : DysonNetwork.Sphere.Auth.Proto.AuthService.AuthServiceBase
|
||||||
|
{
|
||||||
|
private readonly AppDatabase _db;
|
||||||
|
private readonly AccountService _accounts;
|
||||||
|
private readonly AuthService _auth;
|
||||||
|
|
||||||
|
public AuthGrpcService(AppDatabase db, AccountService accounts, AuthService auth)
|
||||||
|
{
|
||||||
|
_db = db;
|
||||||
|
_accounts = accounts;
|
||||||
|
_auth = auth;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<LoginResponse> Login(LoginRequest request, ServerCallContext context)
|
||||||
|
{
|
||||||
|
var account = await _accounts.LookupAccount(request.Username);
|
||||||
|
if (account == null)
|
||||||
|
{
|
||||||
|
throw new RpcException(new Grpc.Core.Status(StatusCode.NotFound, "Account not found."));
|
||||||
|
}
|
||||||
|
|
||||||
|
var factor = await _db.AccountAuthFactors.FirstOrDefaultAsync(f => f.AccountId == account.Id && f.Type == AccountAuthFactorType.Password);
|
||||||
|
if (factor == null || !factor.VerifyPassword(request.Password))
|
||||||
|
{
|
||||||
|
throw new RpcException(new Grpc.Core.Status(StatusCode.Unauthenticated, "Invalid credentials."));
|
||||||
|
}
|
||||||
|
|
||||||
|
var session = new Session
|
||||||
|
{
|
||||||
|
LastGrantedAt = Instant.FromDateTimeUtc(DateTime.UtcNow),
|
||||||
|
ExpiredAt = Instant.FromDateTimeUtc(DateTime.UtcNow.AddDays(30)),
|
||||||
|
Account = account,
|
||||||
|
Challenge = new Challenge() // Create a dummy challenge
|
||||||
|
};
|
||||||
|
|
||||||
|
_db.AuthSessions.Add(session);
|
||||||
|
await _db.SaveChangesAsync();
|
||||||
|
|
||||||
|
var token = _auth.CreateToken(session);
|
||||||
|
|
||||||
|
return new LoginResponse
|
||||||
|
{
|
||||||
|
AccessToken = token,
|
||||||
|
ExpiresIn = (long)(session.ExpiredAt.Value - session.LastGrantedAt.Value).TotalSeconds
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<IntrospectionResponse> IntrospectToken(IntrospectTokenRequest request, ServerCallContext context)
|
||||||
|
{
|
||||||
|
if (_auth.ValidateToken(request.Token, out var sessionId))
|
||||||
|
{
|
||||||
|
var session = await _db.AuthSessions
|
||||||
|
.Include(s => s.Account)
|
||||||
|
.Include(s => s.Challenge)
|
||||||
|
.FirstOrDefaultAsync(s => s.Id == sessionId);
|
||||||
|
|
||||||
|
if (session != null)
|
||||||
|
{
|
||||||
|
return new IntrospectionResponse
|
||||||
|
{
|
||||||
|
Active = true,
|
||||||
|
Claims = JsonSerializer.Serialize(new { sub = session.AccountId }),
|
||||||
|
ClientId = session.AppId?.ToString() ?? "",
|
||||||
|
Username = session.Account.Name,
|
||||||
|
Scope = string.Join(" ", session.Challenge.Scopes),
|
||||||
|
Iat = Timestamp.FromDateTime(session.CreatedAt.ToDateTimeUtc()),
|
||||||
|
Exp = Timestamp.FromDateTime(session.ExpiredAt?.ToDateTimeUtc() ?? DateTime.MaxValue)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new IntrospectionResponse { Active = false };
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<Empty> Logout(Empty request, ServerCallContext context)
|
||||||
|
{
|
||||||
|
var authorizationHeader = context.RequestHeaders.FirstOrDefault(h => h.Key == "authorization");
|
||||||
|
if (authorizationHeader != null)
|
||||||
|
{
|
||||||
|
var token = authorizationHeader.Value.Replace("Bearer ", "");
|
||||||
|
if (_auth.ValidateToken(token, out var sessionId))
|
||||||
|
{
|
||||||
|
var session = await _db.AuthSessions.FindAsync(sessionId);
|
||||||
|
if (session != null)
|
||||||
|
{
|
||||||
|
_db.AuthSessions.Remove(session);
|
||||||
|
await _db.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new Empty();
|
||||||
|
}
|
||||||
|
}
|
@ -164,4 +164,13 @@
|
|||||||
<_ContentIncludedByDefault Remove="app\publish\package.json" />
|
<_ContentIncludedByDefault Remove="app\publish\package.json" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Grpc.AspNetCore" Version="2.65.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Protobuf Include="Protos\auth.proto" GrpcServices="Server" />
|
||||||
|
<Protobuf Include="Protos\account.proto" GrpcServices="Server" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
@ -17,6 +17,9 @@ builder.Services.AddAppRateLimiting();
|
|||||||
builder.Services.AddAppAuthentication();
|
builder.Services.AddAppAuthentication();
|
||||||
builder.Services.AddAppSwagger();
|
builder.Services.AddAppSwagger();
|
||||||
|
|
||||||
|
// Add gRPC services
|
||||||
|
builder.Services.AddGrpc();
|
||||||
|
|
||||||
// Add file storage
|
// Add file storage
|
||||||
builder.Services.AddAppFileStorage(builder.Configuration);
|
builder.Services.AddAppFileStorage(builder.Configuration);
|
||||||
|
|
||||||
@ -44,4 +47,8 @@ var tusDiskStore = app.Services.GetRequiredService<TusDiskStore>();
|
|||||||
// Configure application middleware pipeline
|
// Configure application middleware pipeline
|
||||||
app.ConfigureAppMiddleware(builder.Configuration, tusDiskStore);
|
app.ConfigureAppMiddleware(builder.Configuration, tusDiskStore);
|
||||||
|
|
||||||
|
// Map gRPC services
|
||||||
|
app.MapGrpcService<DysonNetwork.Sphere.Auth.AuthGrpcService>();
|
||||||
|
app.MapGrpcService<DysonNetwork.Sphere.Account.AccountGrpcService>();
|
||||||
|
|
||||||
app.Run();
|
app.Run();
|
28
DysonNetwork.Sphere/Protos/account.proto
Normal file
28
DysonNetwork.Sphere/Protos/account.proto
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
package dyson_network.sphere.account;
|
||||||
|
|
||||||
|
import "google/protobuf/empty.proto";
|
||||||
|
|
||||||
|
option csharp_namespace = "DysonNetwork.Sphere.Account.Proto";
|
||||||
|
|
||||||
|
service AccountService {
|
||||||
|
// Retrieves the current user's account information
|
||||||
|
rpc GetAccount(google.protobuf.Empty) returns (AccountResponse);
|
||||||
|
|
||||||
|
// Updates the current user's account information
|
||||||
|
rpc UpdateAccount(UpdateAccountRequest) returns (AccountResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
message AccountResponse {
|
||||||
|
string id = 1;
|
||||||
|
string username = 2;
|
||||||
|
string email = 3;
|
||||||
|
string display_name = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message UpdateAccountRequest {
|
||||||
|
// Fields to update
|
||||||
|
optional string email = 1;
|
||||||
|
optional string display_name = 2;
|
||||||
|
}
|
55
DysonNetwork.Sphere/Protos/auth.proto
Normal file
55
DysonNetwork.Sphere/Protos/auth.proto
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
package dyson_network.sphere.auth;
|
||||||
|
|
||||||
|
import "google/protobuf/empty.proto";
|
||||||
|
import "google/protobuf/timestamp.proto";
|
||||||
|
|
||||||
|
option csharp_namespace = "DysonNetwork.Sphere.Auth.Proto";
|
||||||
|
|
||||||
|
service AuthService {
|
||||||
|
// Standard username/password login
|
||||||
|
rpc Login(LoginRequest) returns (LoginResponse);
|
||||||
|
|
||||||
|
// Introspects an OAuth 2.0 access token.
|
||||||
|
rpc IntrospectToken(IntrospectTokenRequest) returns (IntrospectionResponse);
|
||||||
|
|
||||||
|
// Logs out the current session
|
||||||
|
rpc Logout(google.protobuf.Empty) returns (google.protobuf.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
message LoginRequest {
|
||||||
|
string username = 1;
|
||||||
|
string password = 2;
|
||||||
|
// Optional: for 2FA
|
||||||
|
string two_factor_code = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message LoginResponse {
|
||||||
|
string access_token = 1;
|
||||||
|
string refresh_token = 2;
|
||||||
|
int64 expires_in = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message IntrospectTokenRequest {
|
||||||
|
string token = 1;
|
||||||
|
// Optional: token_type_hint can be "access_token" or "refresh_token"
|
||||||
|
string token_type_hint = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message IntrospectionResponse {
|
||||||
|
// Indicates whether or not the token is currently active.
|
||||||
|
bool active = 1;
|
||||||
|
// A JSON string containing the claims of the token.
|
||||||
|
string claims = 2;
|
||||||
|
// The client identifier for the OAuth 2.0 client that requested the token.
|
||||||
|
string client_id = 3;
|
||||||
|
// The username of the resource owner who authorized the token.
|
||||||
|
string username = 4;
|
||||||
|
// The scope of the access token.
|
||||||
|
string scope = 5;
|
||||||
|
// The time at which the token was issued.
|
||||||
|
google.protobuf.Timestamp iat = 6;
|
||||||
|
// The time at which the token expires.
|
||||||
|
google.protobuf.Timestamp exp = 7;
|
||||||
|
}
|
@ -7,6 +7,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
|
|||||||
compose.yaml = compose.yaml
|
compose.yaml = compose.yaml
|
||||||
EndProjectSection
|
EndProjectSection
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DysonNetwork.Shared", "DysonNetwork.Shared\DysonNetwork.Shared.csproj", "{C24C6C96-2D99-4D0C-950B-4C3D8B55E930}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
@ -17,5 +19,9 @@ Global
|
|||||||
{CFF62EFA-F4C2-4FC7-8D97-25570B4DB452}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{CFF62EFA-F4C2-4FC7-8D97-25570B4DB452}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{CFF62EFA-F4C2-4FC7-8D97-25570B4DB452}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{CFF62EFA-F4C2-4FC7-8D97-25570B4DB452}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{CFF62EFA-F4C2-4FC7-8D97-25570B4DB452}.Release|Any CPU.Build.0 = Release|Any CPU
|
{CFF62EFA-F4C2-4FC7-8D97-25570B4DB452}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{C24C6C96-2D99-4D0C-950B-4C3D8B55E930}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{C24C6C96-2D99-4D0C-950B-4C3D8B55E930}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{C24C6C96-2D99-4D0C-950B-4C3D8B55E930}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{C24C6C96-2D99-4D0C-950B-4C3D8B55E930}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
EndGlobal
|
EndGlobal
|
||||||
|
Reference in New Issue
Block a user