Started implementing oauth2 based on MoonCore helper services

Its more or less a test how well the helper services improve the implementation. I havent implemented anything fancy here atm. Just testing the oauth2 flow
This commit is contained in:
Masu Baumgartner
2024-10-18 00:03:20 +02:00
parent 13daa3cbac
commit 9d1351527d
13 changed files with 513 additions and 178 deletions

View File

@@ -1,5 +1,12 @@
using Microsoft.AspNetCore.Mvc;
using MoonCore.Extended.Abstractions;
using MoonCore.Extended.Helpers;
using MoonCore.Extended.OAuth2.ApiServer;
using MoonCore.Helpers;
using MoonCore.Services;
using Moonlight.ApiServer.Attributes;
using Moonlight.ApiServer.Configuration;
using Moonlight.ApiServer.Database.Entities;
using Moonlight.ApiServer.Helpers.Authentication;
using Moonlight.ApiServer.Services;
using Moonlight.Shared.Http.Requests.Auth;
@@ -11,39 +18,58 @@ namespace Moonlight.ApiServer.Http.Controllers.Auth;
[Route("api/auth")]
public class AuthController : Controller
{
private readonly AuthService AuthService;
private readonly OAuth2Service OAuth2Service;
private readonly TokenHelper TokenHelper;
private readonly ConfigService<AppConfiguration> ConfigService;
private readonly DatabaseRepository<User> UserRepository;
public AuthController(AuthService authService)
public AuthController(OAuth2Service oAuth2Service, TokenHelper tokenHelper, DatabaseRepository<User> userRepository, ConfigService<AppConfiguration> configService)
{
AuthService = authService;
OAuth2Service = oAuth2Service;
TokenHelper = tokenHelper;
UserRepository = userRepository;
ConfigService = configService;
}
[HttpPost("login")]
public async Task<LoginResponse> Login([FromBody] LoginRequest request)
[HttpGet("start")]
public async Task<AuthStartResponse> Start()
{
var user = await AuthService.Login(request.Email, request.Password);
var data = await OAuth2Service.StartAuthorizing();
return new LoginResponse()
{
Token = await AuthService.GenerateToken(user)
};
return Mapper.Map<AuthStartResponse>(data);
}
[HttpPost("register")]
public async Task<RegisterResponse> Register([FromBody] RegisterRequest request)
[HttpGet("handle")]
public async Task Handle([FromQuery(Name = "code")] string code)
{
var user = await AuthService.Register(
request.Username,
request.Email,
request.Password
);
//TODO: Validate jwt syntax
return new RegisterResponse()
var accessData = await OAuth2Service.RequestAccess(code);
//TODO: Add modular oauth2 consumer system
var userId = 1;
var user = UserRepository.Get().First(x => x.Id == userId);
user.AccessToken = accessData.AccessToken;
user.RefreshToken = accessData.RefreshToken;
user.RefreshTimestamp = DateTime.UtcNow.AddSeconds(accessData.ExpiresIn);
UserRepository.Update(user);
var authConfig = ConfigService.Get().Authentication;
var tokenPair = await TokenHelper.GeneratePair(authConfig.MlAccessSecret, authConfig.MlAccessSecret, data =>
{
Token = await AuthService.GenerateToken(user)
};
data.Add("userId", user.Id.ToString());
});
Response.Cookies.Append("ml-access", tokenPair.AccessToken);
Response.Cookies.Append("ml-refresh", tokenPair.RefreshToken);
Response.Cookies.Append("ml-timestamp", DateTimeOffset.UtcNow.AddSeconds(3600).ToUnixTimeSeconds().ToString());
Response.Redirect("/");
}
[HttpGet("check")]
[RequirePermission("meta.authenticated")]
public async Task<CheckResponse> Check()

View File

@@ -0,0 +1,133 @@
using Microsoft.AspNetCore.Mvc;
using MoonCore.Exceptions;
using MoonCore.Extended.Abstractions;
using MoonCore.Extended.OAuth2.AuthServer;
using MoonCore.Extended.OAuth2.Models;
using MoonCore.Services;
using Moonlight.ApiServer.Configuration;
using Moonlight.ApiServer.Database.Entities;
using Moonlight.ApiServer.Services;
namespace Moonlight.ApiServer.Http.Controllers.OAuth2;
[ApiController]
[Route("oauth2")]
public class OAuth2Controller : Controller
{
private readonly OAuth2Service OAuth2Service;
private readonly AuthService AuthService;
private readonly DatabaseRepository<User> UserRepository;
private readonly ConfigService<AppConfiguration> ConfigService;
public OAuth2Controller(OAuth2Service oAuth2Service, ConfigService<AppConfiguration> configService,
AuthService authService, DatabaseRepository<User> userRepository)
{
OAuth2Service = oAuth2Service;
ConfigService = configService;
AuthService = authService;
UserRepository = userRepository;
}
[HttpGet("authorize")]
public async Task Authorize(
[FromQuery(Name = "response_type")] string responseType,
[FromQuery(Name = "client_id")] string clientId,
[FromQuery(Name = "redirect_uri")] string redirectUri
)
{
if (responseType != "code")
throw new HttpApiException("Invalid response type", 400);
var config = ConfigService.Get();
// TODO: This call should be handled by the OAuth2Service
if (clientId != config.Authentication.ClientId)
throw new HttpApiException("Invalid client id", 400);
if (redirectUri != (config.Authentication.AuthorizationRedirect ?? $"{config.PublicUrl}/api/auth/handle"))
throw new HttpApiException("Invalid redirect uri", 400);
Response.StatusCode = 200;
await Response.WriteAsync(
"<h1>Login lol</h1><br />" +
"<br />" +
"<br />" +
"<form method=\"post\">" +
"<label for=\"email\">Email:</label>" +
"<input type=\"email\" id=\"email\" name=\"email\"><br>" +
"<br>" +
"<label for=\"password\">Password:</label>" +
"<input type=\"password\" id=\"password\" name=\"password\"><br>" +
"<br>" +
"<input type=\"submit\" value=\"Submit\">" +
"</form>"
);
}
[HttpPost("authorize")]
public async Task AuthorizePost(
[FromQuery(Name = "response_type")] string responseType,
[FromQuery(Name = "client_id")] string clientId,
[FromQuery(Name = "redirect_uri")] string redirectUri,
[FromForm(Name = "email")] string email,
[FromForm(Name = "password")] string password
)
{
if (responseType != "code")
throw new HttpApiException("Invalid response type", 400);
var config = ConfigService.Get();
// TODO: This call should be handled by the OAuth2Service
if (clientId != config.Authentication.ClientId)
throw new HttpApiException("Invalid client id", 400);
if (redirectUri != (config.Authentication.AuthorizationRedirect ?? $"{config.PublicUrl}/api/auth/handle"))
throw new HttpApiException("Invalid redirect uri", 400);
var user = await AuthService.Login(email, password);
var code = await OAuth2Service.GenerateCode(data => { data.Add("userId", user.Id.ToString()); });
var redirectUrl = redirectUri +
$"?code={code}";
Response.Redirect(redirectUrl);
}
[HttpPost("access")]
public async Task<AccessData> Access(
[FromForm(Name = "client_id")] string clientId,
[FromForm(Name = "client_secret")] string clientSecret,
[FromForm(Name = "redirect_uri")] string redirectUri,
[FromForm(Name = "grant_type")] string grantType,
[FromForm(Name = "code")] string code
)
{
if (grantType != "authorization_code")
throw new HttpApiException("Invalid grant type", 400);
User? user = null;
var access = await OAuth2Service.ValidateAccess(clientId, clientSecret, redirectUri, code, data =>
{
if (!data.TryGetValue("userId", out var userIdStr))
return false;
if (!int.TryParse(userIdStr, out var userId))
return false;
user = UserRepository.Get().FirstOrDefault(x => x.Id == userId);
return user != null;
}, data =>
{
data.Add("userId", user!.Id.ToString());
});
if (access == null)
throw new HttpApiException("Unable to validate access", 400);
return access;
}
}

View File

@@ -1,6 +1,8 @@
using System.Text.Json;
using MoonCore.Extended.Abstractions;
using MoonCore.Extended.Helpers;
using MoonCore.Extended.Models;
using MoonCore.Extended.OAuth2.ApiServer;
using MoonCore.Services;
using Moonlight.ApiServer.Configuration;
using Moonlight.ApiServer.Database.Entities;
@@ -28,12 +30,88 @@ public class AuthenticationMiddleware
private async Task Authenticate(HttpContext context)
{
var request = context.Request;
if (!request.Cookies.TryGetValue("ml-access", out var accessToken) ||
!request.Cookies.TryGetValue("ml-refresh", out var refreshToken))
return;
if(string.IsNullOrEmpty(accessToken) || string.IsNullOrEmpty(refreshToken))
return;
// TODO: Validate if both are valid jwts (maybe)
//
var tokenHelper = context.RequestServices.GetRequiredService<TokenHelper>();
var configService = context.RequestServices.GetRequiredService<ConfigService<AppConfiguration>>();
User? user = null;
if (!await tokenHelper.IsValidAccessToken(accessToken, configService.Get().Authentication.MlAccessSecret,
data =>
{
if (!data.TryGetValue("userId", out var userIdStr))
return false;
if (!int.TryParse(userIdStr, out var userId))
return false;
var userRepo = context.RequestServices.GetRequiredService<DatabaseRepository<User>>();
user = userRepo.Get().FirstOrDefault(x => x.Id == userId);
return user != null;
}))
{
return;
}
if(user == null)
return;
// Validate external access
if (DateTime.UtcNow > user.RefreshTimestamp)
{
var tokenConsumer = new TokenConsumer(user.AccessToken, user.RefreshToken, user.RefreshTimestamp,
async refreshToken =>
{
var oauth2Service = context.RequestServices.GetRequiredService<OAuth2Service>();
var accessData = await oauth2Service.RefreshAccess(refreshToken);
user.AccessToken = accessData.AccessToken;
user.RefreshToken = accessData.RefreshToken;
user.RefreshTimestamp = DateTime.UtcNow.AddSeconds(accessData.ExpiresIn);
var userRepo = context.RequestServices.GetRequiredService<DatabaseRepository<User>>();
userRepo.Update(user);
return new TokenPair()
{
AccessToken = user.AccessToken,
RefreshToken = user.RefreshToken
};
});
await tokenConsumer.GetAccessToken();
//TODO: API CALL
}
// Load permissions, handle empty values
var permissions = JsonSerializer.Deserialize<string[]>(
string.IsNullOrEmpty(user.PermissionsJson) ? "[]" : user.PermissionsJson
) ?? [];
// Save permission state
context.User = new PermClaimsPrinciple(permissions, user);
/*
string? token = null;
// Cookie for Moonlight.Client
if (request.Cookies.ContainsKey("token") && !string.IsNullOrEmpty(request.Cookies["token"]))
token = request.Cookies["token"];
// Header for api clients
if (request.Headers.ContainsKey("Authorization") && !string.IsNullOrEmpty(request.Cookies["Authorization"]))
{
@@ -47,10 +125,10 @@ public class AuthenticationMiddleware
token = headerParts[1];
}
}
if(token == null)
return;
// Validate token
if (token.Length > 300)
{
@@ -62,7 +140,7 @@ public class AuthenticationMiddleware
if (token.Count(x => x == '.') == 2) // JWT only has two dots
await AuthenticateUser(context, token);
else
await AuthenticateApiKey(context, token);
await AuthenticateApiKey(context, token);*/
}
private async Task AuthenticateUser(HttpContext context, string jwt)
@@ -70,13 +148,13 @@ public class AuthenticationMiddleware
var jwtHelper = context.RequestServices.GetRequiredService<JwtHelper>();
var configService = context.RequestServices.GetRequiredService<ConfigService<AppConfiguration>>();
var secret = configService.Get().Authentication.Secret;
if(!await jwtHelper.Validate(secret, jwt, "login"))
if (!await jwtHelper.Validate(secret, jwt, "login"))
return;
var data = await jwtHelper.Decode(secret, jwt);
if(!data.TryGetValue("iat", out var issuedAtString) || !data.TryGetValue("userId", out var userIdString))
if (!data.TryGetValue("iat", out var issuedAtString) || !data.TryGetValue("userId", out var userIdString))
return;
var userId = int.Parse(userIdString);
@@ -84,12 +162,12 @@ public class AuthenticationMiddleware
var userRepo = context.RequestServices.GetRequiredService<DatabaseRepository<User>>();
var user = userRepo.Get().FirstOrDefault(x => x.Id == userId);
if(user == null)
if (user == null)
return;
// Check if token is in the past
if(user.TokenValidTimestamp > issuedAt)
if (user.TokenValidTimestamp > issuedAt)
return;
// Load permissions, handle empty values
@@ -103,6 +181,5 @@ public class AuthenticationMiddleware
private async Task AuthenticateApiKey(HttpContext context, string apiKey)
{
}
}