Skip to content

Commit 902ca11

Browse files
authored
Merge pull request #451 from Moonlight-Panel/v2.1_ImproveAuth
Improved authentication
2 parents 60178dc + 17cd039 commit 902ca11

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+1570
-851
lines changed

Moonlight.ApiServer/Configuration/AppConfiguration.cs

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using MoonCore.Helpers;
2+
using Moonlight.ApiServer.Implementations.LocalAuth;
23
using YamlDotNet.Serialization;
34

45
namespace Moonlight.ApiServer.Configuration;
@@ -29,6 +30,10 @@ public static AppConfiguration CreateEmpty()
2930
Kestrel = new()
3031
{
3132
AllowedOrigins = []
33+
},
34+
Authentication = new()
35+
{
36+
EnabledSchemes = []
3237
}
3338
};
3439
}
@@ -54,26 +59,21 @@ public record AuthenticationConfig
5459
{
5560
[YamlMember(Description = "The secret token to use for creating jwts and encrypting things. This needs to be at least 32 characters long")]
5661
public string Secret { get; set; } = Formatter.GenerateString(32);
57-
58-
[YamlMember(Description = "The lifespan of generated user tokens in hours")]
59-
public int TokenDuration { get; set; } = 24 * 10;
6062

61-
[YamlMember(Description = "This enables the use of the local oauth2 provider, so moonlight will use itself as an oauth2 provider")]
62-
public bool EnableLocalOAuth2 { get; set; } = true;
63-
public OAuth2Data OAuth2 { get; set; } = new();
63+
[YamlMember(Description = "Settings for the user sessions")]
64+
public SessionsConfig Sessions { get; set; } = new();
6465

65-
public record OAuth2Data
66-
{
67-
public string Secret { get; set; } = Formatter.GenerateString(32);
68-
public string ClientId { get; set; } = Formatter.GenerateString(8);
69-
public string ClientSecret { get; set; } = Formatter.GenerateString(32);
70-
public string? AuthorizationEndpoint { get; set; }
71-
public string? AccessEndpoint { get; set; }
72-
public string? AuthorizationRedirect { get; set; }
66+
[YamlMember(Description = "This specifies if the first registered/synced user will become an admin automatically")]
67+
public bool FirstUserAdmin { get; set; } = true;
7368

74-
[YamlMember(Description = "This specifies if the first registered user will become an admin automatically. This only works when using local oauth2")]
75-
public bool FirstUserAdmin { get; set; } = true;
76-
}
69+
[YamlMember(Description = "This specifies the authentication schemes the frontend should be able to challenge")]
70+
public string[] EnabledSchemes { get; set; } = [LocalAuthConstants.AuthenticationScheme];
71+
}
72+
73+
public record SessionsConfig
74+
{
75+
public string CookieName { get; set; } = "session";
76+
public int ExpiresIn { get; set; } = 10;
7777
}
7878

7979
public record DevelopmentConfig
Lines changed: 88 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,11 @@
1-
using System.IdentityModel.Tokens.Jwt;
2-
using System.Security.Claims;
3-
using System.Text;
1+
using System.Security.Claims;
2+
using Microsoft.AspNetCore.Authentication;
43
using Microsoft.AspNetCore.Authorization;
4+
using Microsoft.AspNetCore.Http;
55
using Microsoft.AspNetCore.Mvc;
6-
using Microsoft.EntityFrameworkCore;
7-
using Microsoft.IdentityModel.Tokens;
8-
using MoonCore.Exceptions;
9-
using MoonCore.Extended.Abstractions;
106
using Moonlight.ApiServer.Configuration;
11-
using Moonlight.ApiServer.Database.Entities;
7+
using Moonlight.ApiServer.Implementations.LocalAuth;
128
using Moonlight.ApiServer.Interfaces;
13-
using Moonlight.Shared.Http.Requests.Auth;
149
using Moonlight.Shared.Http.Responses.Auth;
1510

1611
namespace Moonlight.ApiServer.Http.Controllers.Auth;
@@ -19,93 +14,116 @@ namespace Moonlight.ApiServer.Http.Controllers.Auth;
1914
[Route("api/auth")]
2015
public class AuthController : Controller
2116
{
17+
private readonly IAuthenticationSchemeProvider SchemeProvider;
18+
private readonly IEnumerable<IAuthCheckExtension> Extensions;
2219
private readonly AppConfiguration Configuration;
23-
private readonly DatabaseRepository<User> UserRepository;
24-
private readonly IOAuth2Provider OAuth2Provider;
2520

2621
public AuthController(
27-
AppConfiguration configuration,
28-
DatabaseRepository<User> userRepository,
29-
IOAuth2Provider oAuth2Provider
22+
IAuthenticationSchemeProvider schemeProvider,
23+
IEnumerable<IAuthCheckExtension> extensions,
24+
AppConfiguration configuration
3025
)
3126
{
32-
UserRepository = userRepository;
33-
OAuth2Provider = oAuth2Provider;
27+
SchemeProvider = schemeProvider;
28+
Extensions = extensions;
3429
Configuration = configuration;
3530
}
3631

37-
[AllowAnonymous]
38-
[HttpGet("start")]
39-
public async Task<LoginStartResponse> Start()
32+
[HttpGet]
33+
public async Task<AuthSchemeResponse[]> GetSchemes()
4034
{
41-
var url = await OAuth2Provider.Start();
35+
var schemes = await SchemeProvider.GetAllSchemesAsync();
4236

43-
return new LoginStartResponse()
44-
{
45-
Url = url
46-
};
37+
var allowedSchemes = Configuration.Authentication.EnabledSchemes;
38+
39+
return schemes
40+
.Where(x => allowedSchemes.Contains(x.Name))
41+
.Select(scheme => new AuthSchemeResponse()
42+
{
43+
DisplayName = scheme.DisplayName ?? scheme.Name,
44+
Identifier = scheme.Name
45+
})
46+
.ToArray();
4747
}
4848

49-
[AllowAnonymous]
50-
[HttpPost("complete")]
51-
public async Task<LoginCompleteResponse> Complete([FromBody] LoginCompleteRequest request)
49+
[HttpGet("{identifier:alpha}")]
50+
public async Task StartScheme([FromRoute] string identifier)
5251
{
53-
var user = await OAuth2Provider.Complete(request.Code);
54-
55-
if (user == null)
56-
throw new HttpApiException("Unable to load user data", 500);
52+
// Validate identifier against our enable list
53+
var allowedSchemes = Configuration.Authentication.EnabledSchemes;
5754

58-
// Generate token
59-
var securityTokenDescriptor = new SecurityTokenDescriptor()
55+
if (!allowedSchemes.Contains(identifier))
6056
{
61-
Expires = DateTime.Now.AddHours(Configuration.Authentication.TokenDuration),
62-
IssuedAt = DateTime.Now,
63-
NotBefore = DateTime.Now.AddMinutes(-1),
64-
Claims = new Dictionary<string, object>()
65-
{
66-
{
67-
"userId",
68-
user.Id
69-
},
70-
{
71-
"permissions",
72-
string.Join(";", user.Permissions)
73-
}
74-
},
75-
SigningCredentials = new SigningCredentials(
76-
new SymmetricSecurityKey(
77-
Encoding.UTF8.GetBytes(Configuration.Authentication.Secret)
78-
),
79-
SecurityAlgorithms.HmacSha256
80-
),
81-
Issuer = Configuration.PublicUrl,
82-
Audience = Configuration.PublicUrl
83-
};
57+
await Results
58+
.Problem(
59+
"Invalid scheme identifier provided",
60+
statusCode: 404
61+
)
62+
.ExecuteAsync(HttpContext);
8463

85-
var jwtSecurityTokenHandler = new JwtSecurityTokenHandler();
86-
var securityToken = jwtSecurityTokenHandler.CreateToken(securityTokenDescriptor);
64+
return;
65+
}
8766

88-
var jwt = jwtSecurityTokenHandler.WriteToken(securityToken);
67+
// Now we can check if it even exists
68+
var scheme = await SchemeProvider.GetSchemeAsync(identifier);
8969

90-
return new()
70+
if (scheme == null)
9171
{
92-
AccessToken = jwt
93-
};
72+
await Results
73+
.Problem(
74+
"Invalid scheme identifier provided",
75+
statusCode: 404
76+
)
77+
.ExecuteAsync(HttpContext);
78+
79+
return;
80+
}
81+
82+
// Everything fine, challenge the frontend
83+
await HttpContext.ChallengeAsync(
84+
scheme.Name,
85+
new AuthenticationProperties()
86+
{
87+
RedirectUri = "/"
88+
}
89+
);
9490
}
9591

9692
[Authorize]
9793
[HttpGet("check")]
98-
public async Task<CheckResponse> Check()
94+
public async Task<AuthClaimResponse[]> Check()
9995
{
100-
var userIdStr = User.FindFirstValue("userId")!;
101-
var userId = int.Parse(userIdStr);
102-
var user = await UserRepository.Get().FirstAsync(x => x.Id == userId);
96+
var username = User.FindFirstValue(ClaimTypes.Name)!;
97+
var id = User.FindFirstValue(ClaimTypes.NameIdentifier)!;
98+
var email = User.FindFirstValue(ClaimTypes.Email)!;
99+
var userId = User.FindFirstValue("UserId")!;
100+
var permissions = User.FindFirstValue("Permissions")!;
103101

104-
return new()
102+
// Create basic set of claims used by the frontend
103+
var claims = new List<AuthClaimResponse>()
105104
{
106-
Email = user.Email,
107-
Username = user.Username,
108-
Permissions = user.Permissions
105+
new(ClaimTypes.Name, username),
106+
new(ClaimTypes.NameIdentifier, id),
107+
new(ClaimTypes.Email, email),
108+
new("UserId", userId),
109+
new("Permissions", permissions)
109110
};
111+
112+
// Enrich the frontend claims by extensions (used by plugins)
113+
foreach (var extension in Extensions)
114+
{
115+
claims.AddRange(
116+
await extension.GetFrontendClaims(User)
117+
);
118+
}
119+
120+
return claims.ToArray();
121+
}
122+
123+
[HttpGet("logout")]
124+
public async Task Logout()
125+
{
126+
await HttpContext.SignOutAsync();
127+
await Results.Redirect("/").ExecuteAsync(HttpContext);
110128
}
111129
}

0 commit comments

Comments
 (0)