Skip to content

Commit cdeebe6

Browse files
committed
adds default picture, simplify claims, adds support for cookie as well as bearer
1 parent 856cc34 commit cdeebe6

12 files changed

Lines changed: 72 additions & 46 deletions

CodeWorks.Auth.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
<Nullable>enable</Nullable>
77

88
<PackageId>CodeWorks.Auth</PackageId>
9-
<Version>0.0.1</Version>
9+
<Version>0.0.2</Version>
1010
<Authors>CodeWorks</Authors>
1111
<Company>CodeWorks</Company>
1212
<Description>Flexible, pluggable authentication module for .NET APIs with JWT, email login, and MFA support.</Description>

Extensions/ServiceCollectionExtensions.cs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,26 @@ public static IServiceCollection AddAuthModule<TAccountIdentity, TAccountIdentit
3636

3737
options.Events = new JwtBearerEvents
3838
{
39+
40+
OnMessageReceived = context =>
41+
{
42+
// Check for Authorization header first
43+
var authHeader = context.Request.Headers["Authorization"].FirstOrDefault();
44+
if (!string.IsNullOrEmpty(authHeader) && authHeader.StartsWith("Bearer "))
45+
{
46+
context.Token = authHeader["Bearer ".Length..].Trim();
47+
}
48+
49+
// Fallback to access-token cookie if no token found
50+
if (string.IsNullOrEmpty(context.Token) &&
51+
context.Request.Cookies.TryGetValue(jwtOptions.CookieName, out var cookieToken))
52+
{
53+
context.Token = cookieToken;
54+
}
55+
56+
return Task.CompletedTask;
57+
},
58+
3959
OnAuthenticationFailed = context =>
4060
{
4161
Console.WriteLine("Token invalid: " + context.Exception.Message);
@@ -64,7 +84,6 @@ public static IServiceCollection AddAuthModule<TAccountIdentity, TAccountIdentit
6484
ValidAudience = jwtOptions.Audience,
6585
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtOptions.SigningKey)),
6686
ClockSkew = TimeSpan.Zero,
67-
6887
RoleClaimType = ClaimTypes.Role,
6988
NameClaimType = ClaimTypes.Email
7089
};

Interfaces/IAccountIdentity.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ public interface IAccountIdentity
44
{
55
string Id { get; set; }
66
string Email { get; set; }
7-
string? Name { get; set; }
8-
string? Picture { get; set; }
7+
string Name { get; set; }
8+
string Picture { get; set; }
99

1010
string PasswordHash { get; set; }
1111
bool IsEmailVerified { get; set; }

Interfaces/IAccountIdentityStore.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@ namespace CodeWorks.Auth.Interfaces;
22

33
public interface IAccountIdentityStore<TIdentity> where TIdentity : IAccountIdentity
44
{
5-
Task<TIdentity?> FindByEmailAsync(string email);
5+
Task<TIdentity> FindByEmailAsync(string email);
66
Task<bool> EmailExistsAsync(string email);
77
Task SaveAsync(TIdentity user);
8-
Task<TIdentity?> FindByIdAsync(string id);
8+
Task<TIdentity> FindByIdAsync(string id);
99

1010
async public Task<TIdentity> MarkEmailVerifiedAsync(TIdentity user)
1111
{

Interfaces/IAccountIdentityTokenStore.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,6 @@ namespace CodeWorks.Auth.Interfaces;
33
public interface IAccountIdentityTokenStore
44
{
55
Task SaveTokenAsync(TokenRecord token);
6-
Task<TokenRecord?> GetValidTokenAsync(string token, EmailTokenPurpose purpose);
6+
Task<TokenRecord> GetValidTokenAsync(string token, EmailTokenPurpose purpose);
77
Task MarkTokenUsedAsync(string token);
88
}

Interfaces/IUserTokenStore.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,6 @@ namespace CodeWorks.Auth.Interfaces;
33
public interface IUserTokenStore
44
{
55
Task SaveTokenAsync(TokenRecord token);
6-
Task<TokenRecord?> GetValidTokenAsync(string token, EmailTokenPurpose purpose);
6+
Task<TokenRecord> GetValidTokenAsync(string token, EmailTokenPurpose purpose);
77
Task MarkTokenUsedAsync(string token);
88
}

Models/AuthResult.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@
33
public class AuthResult
44
{
55
public bool IsSuccessful { get; init; }
6+
public IAccountIdentity? User { get; init; }
67
public string? Token { get; init; }
78
public string? Error { get; init; }
89

9-
public static AuthResult Success(IAccountIdentity user, string token) => new() { IsSuccessful = true, Token = token };
10+
public static AuthResult Success(IAccountIdentity user, string token) => new() { IsSuccessful = true, Token = token, User = user };
1011
public static AuthResult Failure(string message) => new() { IsSuccessful = false, Error = message };
1112
}

Models/JwtOptions.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@ public class JwtOptions
44
public string Issuer { get; set; } = "default-issuer";
55
public string Audience { get; set; } = "default-audience";
66
public TimeSpan Expiration { get; set; } = TimeSpan.FromHours(1);
7+
public string CookieName { get; set; } = "access-token";
78
}

Security/ClaimsExtensions.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ public static IEnumerable<string> GetRoles(this ClaimsPrincipal user) =>
1515
{
1616
return new TUser
1717
{
18-
Id = user.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? "",
19-
Email = user.FindFirst(ClaimTypes.Email)?.Value ?? "",
18+
Id = user.FindFirst("id")?.Value ?? user.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? "",
19+
Email = user.FindFirst("email")?.Value ?? user.FindFirst(ClaimTypes.Email)?.Value ?? "",
2020
Name = user.FindFirst("name")?.Value ?? user.FindFirst(ClaimTypes.Email)?.Value ?? "",
2121
Picture = user.FindFirst("picture")?.Value ?? "",
2222
IsEmailVerified = user.FindFirst("email_verified")?.Value == "true",

Security/JWTService.cs

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,21 +19,23 @@ public string GenerateToken(IAccountIdentity user)
1919
{
2020
var claims = new List<Claim>
2121
{
22-
new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
23-
new Claim(ClaimTypes.Email, user.Email),
22+
new(ClaimTypes.NameIdentifier, user.Id.ToString()),
23+
new(ClaimTypes.Email, user.Email),
2424
};
2525

26-
var roles = user.Roles?.ToArray() ?? Array.Empty<string>();
27-
claims.AddRange(roles.Select(r => new Claim(ClaimTypes.Role, r)).ToList());
26+
user.Name ??= user.Email.Substring(0, user.Email.IndexOf('@'));
27+
user.Picture ??= "https://ui-avatars.com/api/?name=" + user.Name + "&color=fff&background=" + StringToHex(user.Email);
2828

29+
claims.Add(new Claim("name", user.Name));
30+
claims.Add(new Claim("picture", user.Picture));
31+
claims.Add(new Claim("id", user.Id));
32+
claims.Add(new Claim("email", user.Email));
2933

30-
var permissions = user.Permissions?.ToArray() ?? Array.Empty<string>();
31-
claims.AddRange(permissions.Select(p => new Claim("permission", p)).ToList());
34+
var roles = user.Roles?.ToArray() ?? [];
35+
claims.AddRange([.. roles.Select(r => new Claim(ClaimTypes.Role, r))]);
3236

33-
if (!string.IsNullOrEmpty(user.Name))
34-
claims.Add(new Claim("name", user.Name));
35-
if (!string.IsNullOrEmpty(user.Picture))
36-
claims.Add(new Claim("picture", user.Picture));
37+
var permissions = user.Permissions?.ToArray() ?? [];
38+
claims.AddRange([.. permissions.Select(p => new Claim("permission", p))]);
3739

3840
claims.Add(new Claim("email_verified", user.IsEmailVerified.ToString()));
3941

@@ -69,10 +71,8 @@ public string GenerateToken(IAccountIdentity user)
6971
ValidAudience = _options.Audience,
7072
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_options.SigningKey)),
7173
ClockSkew = TimeSpan.Zero,
72-
7374
RoleClaimType = ClaimTypes.Role,
7475
NameClaimType = ClaimTypes.Email
75-
7676
}, out _);
7777
}
7878
catch
@@ -103,4 +103,22 @@ public string GetEmailFromToken(string token)
103103
var emailClaim = jwtToken.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Email || c.Type == "email");
104104
return emailClaim?.Value ?? string.Empty;
105105
}
106+
107+
108+
public static string StringToHex(string str = "")
109+
{
110+
try
111+
{
112+
if (string.IsNullOrEmpty(str)) return string.Empty;
113+
if (str.Length > 6) str = str[..6];
114+
return string.Join("", str.Select(c => ((int)c).ToString("x2")))[..6];
115+
}
116+
catch (Exception e)
117+
{
118+
Console.WriteLine(e.Message);
119+
return string.Empty;
120+
}
121+
}
122+
123+
106124
}

0 commit comments

Comments
 (0)